MediaKit.EntityFrameworkCore 1.8.2

dotnet add package MediaKit.EntityFrameworkCore --version 1.8.2
                    
NuGet\Install-Package MediaKit.EntityFrameworkCore -Version 1.8.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MediaKit.EntityFrameworkCore" Version="1.8.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MediaKit.EntityFrameworkCore" Version="1.8.2" />
                    
Directory.Packages.props
<PackageReference Include="MediaKit.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MediaKit.EntityFrameworkCore --version 1.8.2
                    
#r "nuget: MediaKit.EntityFrameworkCore, 1.8.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MediaKit.EntityFrameworkCore@1.8.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MediaKit.EntityFrameworkCore&version=1.8.2
                    
Install as a Cake Addin
#tool nuget:?package=MediaKit.EntityFrameworkCore&version=1.8.2
                    
Install as a Cake Tool

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

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 a Dictionary<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 MediaProcessingHostedService processes them asynchronously.
  • Deletion queue: Old files from single-item collection replacements are queued for background deletion.
  • Bounded channels: When the queue is full, QueueAsync blocks 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_Collection
    • IX_Media_EntityType_Collection
    • IX_Media_EntityId_EntityType
    • IX_Media_CreatedAt_Id_Desc
    • IX_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 Product entity
  • IHasMedia, IHasMediaCollections, and IHasMediaConversions implementations
  • 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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