MediaKit.EntityFrameworkCore
1.5.0
See the version list below for details.
dotnet add package MediaKit.EntityFrameworkCore --version 1.5.0
NuGet\Install-Package MediaKit.EntityFrameworkCore -Version 1.5.0
<PackageReference Include="MediaKit.EntityFrameworkCore" Version="1.5.0" />
<PackageVersion Include="MediaKit.EntityFrameworkCore" Version="1.5.0" />
<PackageReference Include="MediaKit.EntityFrameworkCore" />
paket add MediaKit.EntityFrameworkCore --version 1.5.0
#r "nuget: MediaKit.EntityFrameworkCore, 1.5.0"
#:package MediaKit.EntityFrameworkCore@1.5.0
#addin nuget:?package=MediaKit.EntityFrameworkCore&version=1.5.0
#tool nuget:?package=MediaKit.EntityFrameworkCore&version=1.5.0
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 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 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")
};
}
Register metadata at startup:
MediaCollectionRegistry.Register<Product, Media>(Product.GetCollectionMetadata());
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";
})
Azure Blob Storage
.AddAzureBlobStorage(config =>
{
config.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=...";
config.ContainerName = "media";
config.CreateContainerIfNotExists = true; // Default: true
config.CustomDomain = "cdn.example.com"; // Optional custom domain
})
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, 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.5.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.3)
-
net8.0
- MediaKit (>= 1.5.0)
- 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 | 272 | 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 | 120 | 3/12/2026 |
| 1.6.3 | 125 | 3/12/2026 |
| 1.6.2 | 119 | 3/5/2026 |
| 1.6.1 | 129 | 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 |