MediaKit.EntityFrameworkCore 1.2.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package MediaKit.EntityFrameworkCore --version 1.2.2
                    
NuGet\Install-Package MediaKit.EntityFrameworkCore -Version 1.2.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.2.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MediaKit.EntityFrameworkCore" Version="1.2.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.2.2
                    
#r "nuget: MediaKit.EntityFrameworkCore, 1.2.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.2.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.2.2
                    
Install as a Cake Addin
#tool nuget:?package=MediaKit.EntityFrameworkCore&version=1.2.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
MediaKit.AspNetCore ASP.NET Core integration — MediaService, ImageSharp processing, background workers, file validation, DI registration. net8.0
MediaKit.EntityFrameworkCore EF Core integration — generic entity configuration, query extensions, media eager loading. net8.0
MediaKit.Dapper Dapper integration — type handlers, repository, query extensions for high-performance media access. net8.0, net9.0
MediaKit.Storage Storage providers — Local filesystem, AWS S3, SeaweedFS, RustFS. Resilient uploads with Polly. net8.0, net9.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 providers (at least one required)
dotnet add package MediaKit.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 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";
})

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.

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 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