MilliOrm 1.1.0
dotnet add package MilliOrm --version 1.1.0
NuGet\Install-Package MilliOrm -Version 1.1.0
<PackageReference Include="MilliOrm" Version="1.1.0" />
<PackageVersion Include="MilliOrm" Version="1.1.0" />
<PackageReference Include="MilliOrm" />
paket add MilliOrm --version 1.1.0
#r "nuget: MilliOrm, 1.1.0"
#:package MilliOrm@1.1.0
#addin nuget:?package=MilliOrm&version=1.1.0
#tool nuget:?package=MilliOrm&version=1.1.0
MilliOrm – Lightweight ORM for MS SQL Server
MilliOrm is a lightweight, fast ORM for Microsoft SQL Server designed for developers who want simplicity, performance, and clean mapping without the heavy abstractions of Entity Framework.
It provides LINQ‑style filtering, attribute‑based entity mapping, dependency‑injection support, bulk operations, and raw SQL execution — all in a minimal and intuitive API.
Features
- Built for Microsoft Sql Server
- Fast – optimized SQL generation with minimal overhead
- Simple – small API surface, easy to learn
- Optional attribute-based mapping –
[PrimaryKey],[AutoIncrement],[TableName] - Bulk operations – high‑performance bulk insert & update
- Raw SQL support
- Built-in dependency injection
- LINQ expression filters
Installation
dotnet add package MilliOrm
Registering MilliOrm (Dependency Injection)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMilliOrm(
builder.Configuration.GetConnectionString("DefaultConnection"),
pluralizeTableNames: false
);
This registers IDatabase with scoped lifetime and automatically configures the ORM.
Usage in class:
public class CustomerService
{
private readonly IDatabase _db;
public CustomerService(IDatabase db)
{
_db = db;
}
}
Entity Mapping
[TableName("dbo.Customers")]
public class Customer
{
[PrimaryKey]
[AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}
Supported attributes
| Attribute | Purpose |
|---|---|
PrimaryKey |
Marks primary key column. Optional - required for update |
AutoIncrement |
Identity (ID) auto-generated by SQL Server. Optional - required for insert if available |
TableName("Schema.Table") |
Overrides table name. Optional - required if table name is different from class name or schema is used |
Supported expressions
- Logical operators such as
&&,||,! - Greater than, less than, equal to:
>,<,>=,<=,==,!= - Null handling 'x == null' and 'x != null'
- Auto-expanded bool check (e.g.
c => c.IsActivebecomesc.IsActive = true) - DateTime comparisons
- String methods:
StartsWith,EndsWith,Contains,Equals - Contains methods for arrays and lists:
list.Contains(x.Property), works for lists, arrays, enumerables of primitives
Note: for date time comparissons initialize datetime value outside of the expression, that is use:
var cutoff = DateTime.UtcNow.AddDays(-7);
var recent = await db.GetAsync<Customer>(c => c.CreatedAt >= cutoff);
instead of:
var recent = await db.GetAsync<Customer>(c => c.CreatedAt >= DateTime.UtcNow.AddDays(-7));
NOT Supported expressions
- Any method outside string or collection .Contains
- Mathematical expressions
- Indexers
- MemberInit / New expressions
- Navigation properties (ORM is not relational)
API Usage Examples
Querying Data
var customers = await db.GetAsync<Customer>();
var active = await db.GetAsync<Customer>(
c => c.IsActive,
orderBy: c => c.Name
);
var first100 = await db.GetAsync<Customer>(
c => c.IsActive,
top: 100,
orderBy: c => c.CreatedAt,
ascending: true
);
var first100WithSkip = await db.GetAsync<Customer>(
c => c.IsActive,
top: 100,
orderBy: c => c.CreatedAt,
ascending: true,
skip: 100
);
var first = await db.GetFirstAsync<Customer>(c => c.Id > 0);
More complex example:
var currentDay = DateTime.Now;
var employees = await _database
.GetAsync<Employee>(where: x => x.Name.Contains("Joe") && !x.IsDeleted &&
(x.EmploymentEnd > currentDay || x.EmploymentEnd == null),
top: 10,
orderBy: x => x.Id
);
Generated SQL:
SELECT TOP 10 *
FROM [Employee]
WHERE ((Name LIKE '%' + 'Joe' + '%' AND NOT (IsDeleted = 1))
AND (EmploymentEnd > '2025-12-02' OR EmploymentEnd IS NULL))
ORDER BY [Id] ASC
Insert Data
var customer = new Customer {
Name = "John",
CreatedAt = DateTime.UtcNow,
IsActive = true
};
var id = await db.AddAsync(customer);
Insert multiple (up to 1000 rows):
await db.AddAsync(customers);
Bulk insert:
await db.BulkAddAsync(customers);
Update Data
await db.UpdateAsync(customer);
Update multiple (up to 1000 rows):
await db.UpdateAsync(customers);
Bulk update:
await db.BulkAddAsync(customers);
Update a single column for many rows:
await db.UpdateAsync<Customer>(
c => c.IsActive, false,
c => c.CreatedAt < DateTime.UtcNow.AddYears(-1)
);
Delete Data
await db.DeleteAsync<Customer>(c => !c.IsActive);
Raw SQL Support
await db.ExecuteRawCommandAsync("DELETE FROM dbo.Customers WHERE Id = 1");
var list = await db.ExecuteRawQueryAsync<Customer>(
"SELECT * FROM dbo.Customers"
);
var ids = await db.ExecuteRawQueryScalarAsync<int>(
"SELECT Id FROM dbo.Customers"
);
Transaction support
MilliOrm supports native .Net transactions. Example usage:
try
{
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
var Id = await _database.AddAsync(employee);
var employeeSkill = new EmployeeSkill()
{
EmployeeId = Id,
SkillName = "C# Programming",
ProficiencyLevel = "Advanced"
};
await _database.AddAsync(employeeSkill);
scope.Complete();
}
}
catch(Exception e)
{
//Rollback is automatic if Complete is not called
}
List of all available methods
Task<List<T>> GetAsync<T>(Expression<Func<T, bool>> where = null, int? top = null, Expression<Func<T, object>> orderBy = null, bool ascending = true, int? skip = null) where T : new();
Task<T> GetFirstAsync<T>(Expression<Func<T, bool>> where = null) where T : new();
Task AddAsync<T>(List<T> items);
Task<int?> AddAsync<T>(T item);
Task<int> DeleteAsync<T>(Expression<Func<T, bool>> where = null);
Task UpdateAsync<T>(List<T> items);
Task UpdateAsync<T>(T item);
Task<int> UpdateAsync<T>(Expression<Func<T, object>> column, object value, Expression<Func<T, bool>> where = null);
Task BulkAddAsync<T>(List<T> items);
Task BulkUpdateAsync<T>(List<T> items);
Task<int> ExecuteRawCommandAsync(string sql);
Task<List<T>> ExecuteRawQueryAsync<T>(string sql) where T : new();
Task<List<T>> ExecuteRawQueryScalarAsync<T>(string sql);
Benchmarks
CPU: AMD Ryzen 7 5700U
.NET 8.0 — BenchmarkDotNet v0.15.6
Note that results are based on Dapper Plus and EF Core Bulk Extensions libraries for bulk operation. Bulk operations are included by default in MilliOrm library.
| Method | Mean | Error | StdDev | Median |
|---|---|---|---|---|
| Insert 1000 MilliOrm | 7.115 ms | 0.3963 ms | 1.0915 ms | 6.482 ms |
| Insert 1000 Dapper Plus | 5.984 ms | 0.5092 ms | 1.3324 ms | 5.375 ms |
| Insert 1000 EF Core Bulk ext | 6.862 ms | 0.1372 ms | 0.2772 ms | 6.861 ms |
| Insert 100 000 Milli Orm | 264.136 ms | 9.3938 ms | 25.0739 ms | 256.356 ms |
| Insert 100 000 Dapper Plus | 211.111 ms | 12.4168 ms | 33.5696 ms | 194.697 ms |
| Insert 100 000 EF Core Bulk ext | 532.204 ms | 47.2096 ms | 129.2355 ms | 482.631 ms |
| Method | Mean | Error | StdDev | Median |
|---|---|---|---|---|
| Query First MilliOrm | 143.1 us | 3.80 us | 10.84 us | 146.2 us |
| Query First Dapper | 115.5 us | 2.17 us | 5.80 us | 110.0 us |
| Query First EFCore | 137.1 us | 1.69 us | 1.50 us | 136.6 us |
| Query 1000 MilliOrm | 1,341.1 us | 26.35 us | 41.03 us | 1,338.6 us |
| Query 1000 Dapper | 1,211.3 us | 24.04 us | 37.43 us | 1,216.4 us |
| Query 1000 EFCore | 1,235.3 us | 24.43 us | 61.73 us | 1,213.1 us |
| Query 1 000 000 MilliOrm | 1,081,293.3 us | 19,557.71 us | 18,294.30 us | 1,075,451.4 us |
| Query 1 000 000 Dapper | 792,524.0 us | 15,131.16 us | 19,136.07 us | 787,444.6 us |
| Query 1 000 000 EFCore | 1,205,133.4 us | 22,437.67 us | 22,036.79 us | 1,203,073.6 us |
MilliOrm performs close to Dapper and significantly faster than EF Core for large inserts and large result sets.
When to Use MilliOrm
Use MilliOrm if you want:
- You use Microsoft SQL Server as your database
- Lightweight library
- Minimal abstractions
- High performance
- Easier debugging than EF Core
- Simpler mental model compared to ORMs with change tracking
License
MIT License
Support
If you like the project, star it on GitHub and contribute!
| 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 was computed. 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. |
-
net8.0
- Microsoft.Data.SqlClient (>= 6.1.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Added support for Skip