MediaKit.EntityFrameworkCore
1.8.2
dotnet add package MediaKit.EntityFrameworkCore --version 1.8.2
NuGet\Install-Package MediaKit.EntityFrameworkCore -Version 1.8.2
<PackageReference Include="MediaKit.EntityFrameworkCore" Version="1.8.2" />
<PackageVersion Include="MediaKit.EntityFrameworkCore" Version="1.8.2" />
<PackageReference Include="MediaKit.EntityFrameworkCore" />
paket add MediaKit.EntityFrameworkCore --version 1.8.2
#r "nuget: MediaKit.EntityFrameworkCore, 1.8.2"
#:package MediaKit.EntityFrameworkCore@1.8.2
#addin nuget:?package=MediaKit.EntityFrameworkCore&version=1.8.2
#tool nuget:?package=MediaKit.EntityFrameworkCore&version=1.8.2
MediaKit
Polymorphic media management library for .NET — file attachments, multi-provider storage, image processing, and background conversions without owning your app architecture.
MediaKit gives you production-grade media handling for any .NET API. Attach files to any entity, store them on local disk or S3-compatible services, generate image conversions, and query media with EF Core or Dapper — all with a fluent builder API. It fills the Spatie Media Library gap for the .NET ecosystem.
Table of Contents
- Packages
- Installation
- Quick Start
- Core Concepts
- Configuration
- Storage Providers
- Image Processing
- Background Processing
- File Validation
- Media Service
- Entity Extensions
- EF Core Integration
- Dapper Integration
- URL Building
- OpenTelemetry
- Sample Application
- License
Packages
| Package | Description | Target |
|---|---|---|
| MediaKit | Core types — media entity, interfaces, DTOs, validation, extensions. Zero ASP.NET dependency. | net8.0, net9.0, net10.0 |
| MediaKit.AspNetCore | ASP.NET Core integration — MediaService, ImageSharp processing, background workers, file validation, DI registration. | net8.0, net10.0 |
| MediaKit.EntityFrameworkCore | EF Core integration — generic entity configuration, query extensions, media eager loading. | net8.0, net10.0 |
| MediaKit.Dapper | Dapper integration — type handlers, repository, query extensions for high-performance media access. | net8.0, net9.0, net10.0 |
| MediaKit.Storage | Meta-package — includes S3 and Local providers for convenience. | net8.0, net9.0, net10.0 |
| MediaKit.Storage.S3 | S3-compatible storage — AWS S3, SeaweedFS, RustFS, Garage with Polly resilience. | net8.0, net9.0, net10.0 |
| MediaKit.Storage.Local | Local filesystem storage with JWT signed URLs. | net8.0, net9.0, net10.0 |
| MediaKit.Storage.AzureBlobs | Azure Blob Storage with SAS signed URLs and Polly resilience. | net8.0, net9.0, net10.0 |
| MediaKit.Storage.Abstractions | Shared utilities — key generation, resilience pipelines, file name sanitization. | net8.0, net9.0, net10.0 |
Installation
# Core (required)
dotnet add package MediaKit
# ASP.NET Core integration (required for web apps)
dotnet add package MediaKit.AspNetCore
# EF Core entity configuration and query helpers
dotnet add package MediaKit.EntityFrameworkCore
# Dapper integration (alternative to EF Core)
dotnet add package MediaKit.Dapper
# Storage — pick what you need:
dotnet add package MediaKit.Storage # Meta-package (S3 + Local)
dotnet add package MediaKit.Storage.S3 # S3 / SeaweedFS / RustFS / Garage only
dotnet add package MediaKit.Storage.Local # Local filesystem only
dotnet add package MediaKit.Storage.AzureBlobs # Azure Blob Storage
Quick Start
using MediaKit.AspNetCore.DependencyInjection;
using MediaKit.Collections;
using MediaKit.Entities;
using MediaKit.Storage.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Register MediaKit with local storage and ImageSharp
builder.Services.AddMediaKit<AppDbContext, Media>(options =>
{
options.UseBackgroundProcessing = true;
})
.AddLocalStorage(config =>
{
config.RootPath = "wwwroot/storage";
config.BaseUrl = "/storage";
})
.AddImageSharp();
// Register collection metadata
MediaCollectionRegistry.Register<Product, Media>(Product.GetCollectionMetadata());
var app = builder.Build();
app.MapPost("/api/products/{id}/cover", async (
int id, IFormFile file, AppDbContext db,
MediaService<AppDbContext, Media> mediaService) =>
{
var product = await db.Products.FindAsync(id);
var media = await mediaService.UploadAsync(product!, file, "cover");
return Results.Ok(new { media.Id, media.Url, media.Conversions });
});
app.Run();
Core Concepts
Media Entity
The Media class is the base entity for all file attachments. It uses a polymorphic design (EntityId + EntityType) to attach to any entity without foreign keys:
public class Media
{
public string Id { get; set; } // Auto-generated GUID
public string FileName { get; set; } // Storage filename
public string OriginalName { get; set; } // User-facing filename
public string MimeType { get; set; } // Content type
public string Path { get; set; } // Storage path
public string Url { get; set; } // Public URL
public long Size { get; set; } // File size in bytes
public string Collection { get; set; } // "cover", "gallery", "documents"
public string Disk { get; set; } // "local", "s3", "seaweedfs"
public int SortOrder { get; set; } // Ordering within collection
public string? EntityId { get; set; } // Polymorphic owner ID
public string? EntityType { get; set; } // Polymorphic owner type
// JSON-serialized dictionaries
public Dictionary<string, string> Conversions { get; set; } // "thumbnail" => "/storage/.../thumb.webp"
public Dictionary<string, object> CustomProperties { get; set; } // width, height, etc.
}
Extend Media with a derived class for app-specific properties:
public class AppMedia : Media
{
public string? AltText { get; set; }
public string? Caption { get; set; }
}
Polymorphic Attachments
Any entity can have media by implementing IHasMedia<TMedia>:
public class Product : IHasMedia<Media>
{
public int Id { get; set; }
public string Name { get; set; } = "";
// IHasMedia implementation
object IHasMedia<Media>.Id => Id;
public static string MediaEntityType => "product";
public ICollection<Media>? Media { get; set; }
}
public class Author : IHasMedia<Media>
{
public int Id { get; set; }
public string Name { get; set; } = "";
object IHasMedia<Media>.Id => Id;
public static string MediaEntityType => "author";
public ICollection<Media>? Media { get; set; }
}
The MediaEntityType string is stored in the entity_type column, enabling queries like "get all media for product 42" without a direct FK.
Media Collections
Collections organize media by purpose. Define collection behavior with IHasMediaCollections:
public class Product : IHasMedia<Media>, IHasMediaCollections
{
// ...
public static IReadOnlyDictionary<string, MediaCollectionMetadata> GetCollectionMetadata() =>
new Dictionary<string, MediaCollectionMetadata>
{
// Single-item: uploading replaces existing media
["cover"] = new(IsSingleItem: true, MaxItems: 1, Description: "Product cover image"),
// Multi-item: uploading appends to collection
["gallery"] = new(IsSingleItem: false, MaxItems: 10, Description: "Product gallery"),
// Documents collection
["documents"] = new(IsSingleItem: false, MaxItems: 5, Description: "Product documents"),
// Private collection — requires signed URLs for access
["invoices"] = new(IsSingleItem: false, MaxItems: 20, Description: "Private invoices", RequiresSignedUrl: true)
};
}
Register metadata at startup:
MediaCollectionRegistry.Register<Product, Media>(Product.GetCollectionMetadata());
RequiresSignedUrl flag: Collections default to RequiresSignedUrl: false, meaning media is served via public URLs. Set RequiresSignedUrl: true on collections that contain private content (invoices, contracts, etc.) — consumers can use this flag to decide whether to generate signed URLs or return direct public URLs.
Typed Collection Properties
Instead of calling product.GetFirstMedia("cover") (O(n) LINQ scan every time), define typed properties with cached O(1) access using MediaCollectionView<TMedia>:
using MediaKit.Collections;
using MediaKit.Entities;
public class Product : IHasMedia<Media>, IHasMediaCollections
{
public int Id { get; set; }
public string Name { get; set; } = "";
// IHasMedia — unchanged
object IHasMedia<Media>.Id => Id;
public static string MediaEntityType => "product";
public ICollection<Media>? Media { get; set; }
// Cached collection view — single O(n) pass indexes all collections
private MediaCollectionView<Media>? _mediaView;
private MediaCollectionView<Media> MediaView
=> _mediaView ??= new(Media);
// Typed properties — O(1) cached lookup
[MediaCollection("cover", IsSingle = true)]
public Media? Cover => MediaView.GetFirst("cover");
[MediaCollection("gallery")]
public IReadOnlyList<Media> Gallery => MediaView.GetAll("gallery");
// ... collection metadata, conversions, etc.
}
How it works:
MediaCollectionView<TMedia>builds aDictionary<string, List<TMedia>>index in a single O(n) pass on first access to any method- All subsequent property accesses are O(1) dictionary lookups — no LINQ scans, no allocations
[MediaCollection]attribute decorates typed properties so EF Core automatically ignores them (no shadow columns or FKs)Array.Empty<TMedia>()is returned for missing collections (singleton, zero allocation)
Collection-specific database loading:
// Only load cover media from DB — not gallery, not everything
var products = await db.Products
.IncludeMedia<Product, Media>("cover")
.ToListAsync();
// product.Cover is populated, product.Gallery is empty (not loaded)
var coverUrl = products[0].Cover?.Url;
// Load specific collections
var products = await db.Products
.IncludeMedia<Product, Media>("cover", "gallery")
.ToListAsync();
// Load via property expression (resolves collection name from [MediaCollection] attribute)
var products = await db.Products
.IncludeMediaFor<Product, Media>(p => p.Cover)
.ToListAsync();
Mutation helpers:
// Replace single-item collection
product.SetSingleMedia("cover", newMedia);
// Replace multi-item collection
product.SetCollectionMedia("gallery", newGalleryItems);
This feature is fully opt-in — entities without [MediaCollection] properties work exactly as before.
Image Conversions
Define conversions per collection with IHasMediaConversions:
public class Product : IHasMedia<Media>, IHasMediaConversions
{
// ...
public static IReadOnlyDictionary<string, List<ImageConversion>> GetMediaConversions() =>
new Dictionary<string, List<ImageConversion>>
{
["cover"] =
[
new ImageConversion("thumbnail", Width: 300, Height: null), // 300px wide, auto height
new ImageConversion("preview", Width: 800, Height: null), // 800px wide, auto height
new ImageConversion("og-image", Width: 1200, Height: 630, // Exact dimensions
Format: ImageOutputFormat.Jpeg, Quality: 90)
],
["gallery"] =
[
new ImageConversion("thumbnail", Width: 200, Height: 200) // Square thumbnail
]
};
}
Conversions are stored in the Conversions dictionary:
var thumbnailUrl = product.GetFirstMediaUrl("thumbnail", "cover");
// Returns the thumbnail URL, or falls back to original if conversion doesn't exist
Configuration
MediaKit Options
builder.Services.AddMediaKit<AppDbContext, Media>(options =>
{
// Background processing (default: false — conversions run synchronously)
options.UseBackgroundProcessing = true;
// Channel capacity for background queues (backpressure control)
options.BackgroundQueueCapacity = 100;
// OpenTelemetry activity source name
options.ActivitySourceName = "MyApp.Media";
// Fallback base URL when HttpContext is unavailable
options.BaseUrl = "https://api.example.com";
// Default collection name for media without explicit collection
options.DefaultCollection = "default";
});
Builder Pattern
AddMediaKit() returns an IMediaKitBuilder for fluent chaining:
builder.Services.AddMediaKit<AppDbContext, Media>(options =>
{
options.UseBackgroundProcessing = true;
})
.AddS3Storage(config => // Storage provider (required)
{
config.BucketName = "my-media";
config.Region = "us-east-1";
config.CloudFrontDomain = "d123.cloudfront.net";
})
.AddImageSharp() // Image processor (optional)
.AddConversions( // Config-based conversions (optional)
builder.Configuration.GetSection("MediaConversions"));
Storage is validated at startup — if no provider is registered, the app fails fast with a clear error message.
Storage Providers
All storage providers implement IFileStorageService and support upload, download, delete, and signed URLs.
Local Filesystem
.AddLocalStorage(config =>
{
config.RootPath = "wwwroot/storage"; // Physical path on disk
config.BaseUrl = "/storage"; // URL prefix
config.JwtSecretKey = "..."; // Optional: enable signed download URLs
config.JwtIssuer = "MediaKit";
config.JwtAudience = "MediaKit";
})
AWS S3
.AddS3Storage(config =>
{
config.BucketName = "my-media-bucket";
config.Region = "us-east-1";
config.PublicRead = true;
config.CloudFrontDomain = "d123.cloudfront.net"; // Optional CDN
config.CustomDomain = "media.example.com"; // Optional custom domain
})
S3 uploads use Polly resilience pipelines with exponential backoff and jitter for transient error handling.
SeaweedFS
.AddSeaweedFSStorage(config =>
{
config.BucketName = "media";
config.S3Endpoint = "http://seaweedfs:8333";
config.FilerUrl = "http://seaweedfs:8888";
config.AccessKey = "admin";
config.SecretKey = "secret";
})
RustFS
.AddRustFSStorage(config =>
{
config.BucketName = "media";
config.Endpoint = "http://rustfs:9000";
config.AccessKey = "minioadmin";
config.SecretKey = "minioadmin";
config.Region = "us-east-1";
config.PublicRead = true;
config.CustomDomain = "cdn.example.com";
})
RustFS automatically sets a public-read bucket policy (s3:GetObject for all principals) on first upload, so media URLs work without signed access. The policy is applied once and cached for the lifetime of the service.
Garage
.AddGarageStorage(config =>
{
config.BucketName = "media";
config.Endpoint = "http://garage:3900";
config.AccessKey = "GKxxxxxxxxxxxxxxxx";
config.SecretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
config.Region = "garage"; // Default: "garage"
config.PublicUrl = "http://garage:3900"; // Optional: public-facing URL
})
Garage is a lightweight, self-hosted, S3-compatible distributed object storage system. It uses path-style URLs and defaults to the garage region. Requires cluster layout initialization and key/bucket creation before first use (see Garage Admin API).
Azure Blob Storage
.AddAzureBlobStorage(config =>
{
config.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=...";
config.ContainerName = "media";
config.CreateContainerIfNotExists = true; // Default: true
config.PublicAccess = true; // Default: true — sets PublicAccessType.Blob on container
config.CustomDomain = "cdn.example.com"; // Optional custom domain
})
When PublicAccess is true (the default), the container is created with PublicAccessType.Blob, allowing anonymous read access to blobs via their URLs. Set to false to require SAS signed URLs for all access.
Azure Blob Storage uses Polly resilience pipelines and generates SAS signed URLs for secure direct access.
Custom Storage Provider
Register your own IFileStorageService implementation without creating a NuGet package:
// Simple class registration
builder.Services.AddMediaKit<AppDbContext, Media>()
.AddCustomStorage<MyMinioStorageService>();
// With typed options
builder.Services.AddMediaKit<AppDbContext, Media>()
.AddCustomStorage<MyMinioStorageService, MyMinioOptions>(o =>
{
o.Endpoint = "http://localhost:9000";
o.BucketName = "media";
});
// Factory delegate for full control
builder.Services.AddMediaKit<AppDbContext, Media>()
.AddCustomStorage(sp => new MyMinioStorageService(
sp.GetRequiredService<IOptions<MyMinioOptions>>(),
sp.GetRequiredService<ILogger<MyMinioStorageService>>()
));
Auto-detect from Config
Use AddStorage() to select the provider from appsettings.json:
.AddStorage(builder.Configuration)
{
"FileStorage": {
"Type": "s3",
"S3": {
"BucketName": "my-media",
"Region": "us-east-1",
"CloudFrontDomain": "d123.cloudfront.net"
}
}
}
Supported types: s3, seaweedfs, rustfs, garage, local. For Azure Blob Storage, install MediaKit.Storage.AzureBlobs and call .AddAzureBlobStorage() directly.
Image Processing
ImageSharp Integration
Register the built-in ImageSharp processor:
.AddImageSharp()
This registers ImageSharpProcessor as IImageProcessor. It supports:
- Resize (width, height, or both)
- Format conversion (WebP, JPEG, PNG)
- Quality control (1-100)
- Automatic dimension detection
The IImageProcessor interface enables swapping to alternatives (SkiaSharp, Magick.NET):
public interface IImageProcessor
{
Task<IReadOnlyList<ImageConversionResult>> ProcessAsync(
Stream sourceStream, string fileName,
IReadOnlyList<ImageConversion> conversions,
CancellationToken ct = default);
}
Conversion Configuration
Define conversions in appsettings.json as a fallback when entities don't implement IHasMediaConversions:
{
"MediaConversions": {
"Conversions": {
"product": {
"cover": [
{ "Name": "thumbnail", "Width": 300, "Format": "WebP", "Quality": 80 },
{ "Name": "preview", "Width": 800, "Format": "WebP", "Quality": 85 }
]
}
}
}
}
.AddConversions(builder.Configuration.GetSection("MediaConversions"))
Type-safe Conversions
Type-safe conversions via IHasMediaConversions take priority over appsettings.json:
public class Product : IHasMediaConversions
{
public static IReadOnlyDictionary<string, List<ImageConversion>> GetMediaConversions() =>
new Dictionary<string, List<ImageConversion>>
{
["cover"] =
[
new ImageConversion("thumbnail", Width: 300, Height: null,
Format: ImageOutputFormat.WebP, Quality: 80)
]
};
}
Background Processing
When enabled, image conversions and file deletions run in background hosted services using System.Threading.Channels with bounded capacity for backpressure:
options.UseBackgroundProcessing = true;
options.BackgroundQueueCapacity = 200;
- Conversion queue: Image conversions are queued after upload. The
MediaProcessingHostedServiceprocesses them asynchronously. - Deletion queue: Old files from single-item collection replacements are queued for background deletion.
- Bounded channels: When the queue is full,
QueueAsyncblocks until space is available, preventing memory exhaustion.
When UseBackgroundProcessing = false (default), conversions run synchronously within the upload request.
File Validation
Fluent IFormFile Validation
Chain validation methods with a fluent API:
using MediaKit.AspNetCore.Validation;
// Individual validators
file.ValidateNotEmpty()
.ValidateIsImage()
.ValidateSize(maxSizeBytes: 10 * 1024 * 1024)
.ValidateMimeType(["image/jpeg", "image/png", "image/webp"])
.ValidateExtension([".jpg", ".png", ".webp"]);
await file.ValidateSignatureAsync(); // Magic byte verification
await file.ValidateImageDimensionsAsync( // Pixel dimension limits
maxWidth: 4000, maxHeight: 4000);
// Convenience: validate all common image constraints in one call
await file.ValidateImageUploadAsync(maxSizeBytes: 5 * 1024 * 1024);
// Batch validation
files.ValidateFiles(f => f.ValidateNotEmpty().ValidateIsImage().ValidateSize(10_000_000));
All validators throw MediaValidationException with descriptive messages.
File Signature Validation
FileSignatureValidator inspects magic bytes to prevent MIME type spoofing:
using MediaKit.Validation;
var isValid = await FileSignatureValidator.ValidateAsync(stream, "image/jpeg");
var supported = FileSignatureValidator.GetSupportedMimeTypes();
// JPEG, PNG, GIF, WebP, BMP, ICO, SVG, PDF, MP4
Media Service
MediaService<TDbContext, TMedia> orchestrates uploads, conversions, and storage with OpenTelemetry tracing.
Upload
Upload a single file to an entity's collection:
var media = await mediaService.UploadAsync(product, file, "cover");
// For single-item collections, existing media is automatically replaced
// Old files are queued for background deletion
Upload Many
Upload multiple files concurrently:
var results = await mediaService.UploadManyAsync(product, files, "gallery");
// Each file gets its own DI scope for safe concurrent uploads
Replace Collection
Delete all existing media in a collection, then upload a new file:
var media = await mediaService.ReplaceCollectionAsync(product, file, "cover");
Delete
Delete a single media item and its conversions:
var deleted = await mediaService.DeleteAsync(mediaId);
Delete all media in a collection:
var count = await mediaService.DeleteCollectionAsync(product, "gallery");
Entity Extensions
Rich extension methods on IHasMedia<TMedia> for convenient media access:
using MediaKit.Extensions;
// Get media by collection (ordered by SortOrder)
var covers = product.GetMedia("cover");
var galleryImages = product.GetMedia("gallery");
// First media item
var cover = product.GetFirstMedia("cover");
// Check existence
bool hasCover = product.HasMedia("cover");
int galleryCount = product.GetMediaCount("gallery");
// URLs — original or conversion
string? coverUrl = product.GetFirstMediaUrl("cover");
string? thumbUrl = product.GetFirstMediaUrl("thumbnail", "cover");
IEnumerable<string> urls = product.GetMediaUrls("gallery");
// Image dimensions (from CustomProperties)
var dims = product.GetFirstMediaDimensions("cover"); // (int Width, int Height)?
// Filter by MIME type
var images = product.GetImages("gallery");
var videos = product.GetVideos("gallery");
// Get all collection names on an entity
var collections = product.GetMediaCollectionNames();
// Check if a specific conversion exists
bool hasThumb = product.HasMediaConversion("thumbnail", "cover");
// Set/replace media in a single-item collection
product.SetSingleMedia("cover", newCoverMedia);
// Set/replace all media in a collection
product.SetCollectionMedia("gallery", newGalleryItems);
EF Core Integration
Entity Configuration
MediaEntityConfiguration<TMedia> provides a portable configuration with JSON value conversions for Conversions and CustomProperties:
// In your DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new MediaEntityConfiguration<Media>());
}
This configures:
- Table name:
media - Primary key:
Id(max 26 chars) - Column constraints and max lengths
- JSON serialization for dictionary properties
- Composite indexes for polymorphic lookups:
IX_Media_EntityType_EntityId_CollectionIX_Media_EntityType_CollectionIX_Media_EntityId_EntityTypeIX_Media_CreatedAt_Id_DescIX_Media_CreatedAt_Id_Asc
Query Extensions
using MediaKit.EntityFrameworkCore;
// Eager load all media
var products = await db.Products
.IncludeMedia<Product, Media>()
.ToListAsync();
// Eager load specific collection
var products = await db.Products
.IncludeMedia<Product, Media>("cover")
.ToListAsync();
// Eager load multiple specific collections
var products = await db.Products
.IncludeMedia<Product, Media>("cover", "gallery")
.ToListAsync();
// Eager load via typed property expression (resolves from [MediaCollection] attribute)
var products = await db.Products
.IncludeMediaFor<Product, Media>(p => p.Cover)
.ToListAsync();
// Eager load multiple typed properties
var products = await db.Products
.IncludeMediaFor<Product, Media>(p => p.Cover, p => p.Gallery)
.ToListAsync();
// Explicit loading
await product.LoadMediaAsync<Product, Media>(db);
await product.LoadMediaAsync<Product, Media>(db, "cover");
await product.LoadMediaAsync<Product, Media>(db, ["cover", "gallery"]);
// Explicit loading via typed property expression
await product.LoadMediaForAsync<Product, Media>(db, p => p.Cover);
// Filter entities by media existence
var withCovers = await db.Products
.WhereHasMedia<Product, Media>("cover")
.ToListAsync();
// Filter by media count
var withGallery = await db.Products
.WhereHasMediaCount<Product, Media>(3, "gallery")
.ToListAsync();
// Filter by MIME type
var withImages = await db.Products
.WhereHasMediaType<Product, Media>("image/", "gallery")
.ToListAsync();
// Batch preload media for a list of entities
await products.PreloadMediaAsync<Product, Media>(db, "cover");
PostgreSQL Override
Override MediaEntityConfiguration for database-specific column types:
public class AppMediaConfiguration : MediaEntityConfiguration<AppMedia>
{
public override void Configure(EntityTypeBuilder<AppMedia> builder)
{
base.Configure(builder);
// PostgreSQL jsonb columns
builder.Property(m => m.Conversions).HasColumnType("jsonb");
builder.Property(m => m.CustomProperties).HasColumnType("jsonb");
// App-specific columns
builder.Property(m => m.AltText).HasMaxLength(500);
builder.Property(m => m.Caption).HasMaxLength(1000);
// Global query filter (soft delete)
builder.HasQueryFilter(m => !m.IsDeleted);
}
}
Dapper Integration
MediaKit.Dapper provides a lightweight, high-performance alternative to EF Core for media queries. It includes JSON type handlers, a generic repository, and query extensions that work seamlessly with typed collection properties.
Setup
Register Dapper with a standalone connection factory:
using MediaKit.Dapper.DependencyInjection;
builder.Services.AddMediaKit<Media>(options => { })
.AddDapper<Media>(sp => new NpgsqlConnection(connectionString))
.AddLocalStorage(config => { config.RootPath = "wwwroot/storage"; });
Or register type handlers only (for direct Dapper usage without the repository):
builder.Services.AddMediaKit<Media>(options => { })
.AddDapperTypeHandlers();
Repository
IMediaRepository<TMedia> provides full CRUD operations:
using MediaKit.Dapper.Repository;
public class ProductService(IMediaRepository<Media> mediaRepo)
{
public async Task<IReadOnlyList<Media>> GetProductMedia(string productId)
{
return await mediaRepo.GetByEntityAsync("product", productId);
}
public async Task<Media?> GetCover(string productId)
{
return await mediaRepo.GetFirstByEntityAsync("product", productId, "cover");
}
public async Task<int> GetGalleryCount(string productId)
{
return await mediaRepo.CountByEntityAsync("product", productId, "gallery");
}
public async Task<bool> DeleteGallery(string productId)
{
var count = await mediaRepo.DeleteByEntityAsync("product", productId, "gallery");
return count > 0;
}
}
Query Extensions (Dapper)
Extension methods on IDbConnection mirror the EF Core query extensions:
using MediaKit.Dapper.Extensions;
// Load all media for an entity
await connection.LoadMediaAsync<Product, Media>(product);
// Load a specific collection
await connection.LoadMediaAsync<Product, Media>(product, "cover");
// Load multiple collections
await connection.LoadMediaAsync<Product, Media>(product, ["cover", "gallery"]);
// Batch preload media for multiple entities
await connection.PreloadMediaAsync<Product, Media>(products, "cover");
// Check media existence and count
bool hasCover = await connection.HasMediaAsync<Product, Media>(product, "cover");
int count = await connection.GetMediaCountAsync<Product, Media>(product, "gallery");
// Find entities with specific media types
var ids = await connection.GetEntityIdsWithMediaTypeAsync(
"product", "image/", "gallery");
Expression-based Loading
Use typed property expressions to load collections — resolves the collection name from [MediaCollection] attributes:
// Load via property expression
await connection.LoadMediaForAsync<Product, Media>(product, p => p.Cover);
// Load multiple typed collections
await connection.LoadMediaForAsync<Product, Media>(
product,
[p => p.Cover, p => p.Gallery]);
// Typed properties work after Dapper loads the data
var coverUrl = product.Cover?.Url;
var galleryCount = product.Gallery.Count;
Alongside EF Core
Use Dapper alongside EF Core by extracting the connection from your DbContext:
builder.Services.AddMediaKit<AppDbContext, Media>(options => { })
.AddDapper<AppDbContext, Media>();
This resolves AppDbContext from DI and extracts its IDbConnection via reflection — no compile-time EF Core dependency in the Dapper package.
URL Building
IMediaUrlBuilder generates absolute URLs from relative paths, using HttpContext with a configurable fallback:
// Automatically registered — uses HttpContext.Request for scheme/host
// Falls back to MediaKitOptions.BaseUrl when HttpContext is unavailable (background services, CLI tools)
builder.Services.AddMediaKit<AppDbContext, Media>(options =>
{
options.BaseUrl = "https://api.example.com";
});
OpenTelemetry
All MediaService operations emit OpenTelemetry activities with detailed tags:
options.ActivitySourceName = "MyApp.Media"; // Default: "MediaKit"
Activities include:
MediaService.Upload— entity type, entity ID, collection, file size, upload duration, conversion count- Storage upload events with duration
- Database save events with duration
- Image conversion events with duration
Sample Application
A complete working example is at samples/MediaKit.Sample/. It demonstrates:
- SQLite database with
Productentity IHasMedia,IHasMediaCollections, andIHasMediaConversionsimplementations- Local filesystem storage with ImageSharp processing
- Background conversion processing
- Upload, list, and delete endpoints
Run it:
cd samples/MediaKit.Sample
dotnet run
Then test:
# Create a product
curl -X POST http://localhost:5000/api/products -F "name=Widget"
# Upload a cover image (single-item — replaces existing)
curl -X POST http://localhost:5000/api/products/1/cover -F "file=@photo.jpg"
# Upload gallery images (multi-item — appends)
curl -X POST http://localhost:5000/api/products/1/gallery \
-F "files=@img1.jpg" -F "files=@img2.jpg"
# List products with media URLs
curl http://localhost:5000/api/products
# Get product detail with full media info
curl http://localhost:5000/api/products/1
# Delete a media item
curl -X DELETE http://localhost:5000/api/media/{mediaId}
License
MIT -- Copyright (c) 2026 MediaKit Contributors
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MediaKit (>= 1.8.2)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.3)
-
net8.0
- MediaKit (>= 1.8.2)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on MediaKit.EntityFrameworkCore:
| Package | Downloads |
|---|---|
|
MediaKit.AspNetCore
ASP.NET Core integration for MediaKit — MediaService, ImageSharp processing, background tasks, and DI registration. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.8.2 | 270 | 3/22/2026 |
| 1.8.1 | 116 | 3/22/2026 |
| 1.8.0 | 123 | 3/16/2026 |
| 1.7.0 | 118 | 3/16/2026 |
| 1.6.5 | 127 | 3/12/2026 |
| 1.6.4 | 119 | 3/12/2026 |
| 1.6.3 | 125 | 3/12/2026 |
| 1.6.2 | 119 | 3/5/2026 |
| 1.6.1 | 128 | 3/3/2026 |
| 1.5.2 | 115 | 3/5/2026 |
| 1.5.1 | 124 | 3/2/2026 |
| 1.5.0 | 123 | 3/2/2026 |
| 1.4.0 | 122 | 3/2/2026 |
| 1.3.0 | 126 | 3/1/2026 |
| 1.2.2 | 123 | 2/27/2026 |
| 1.2.1 | 125 | 2/26/2026 |
| 1.2.0 | 124 | 2/24/2026 |
| 1.0.0 | 126 | 2/23/2026 |