GoNtext brings the familiar Entity Framework Core patterns to Go, providing a type-safe, LINQ-style ORM with automatic change tracking, migrations, and fluent querying capabilities.
- 🎯 EF Core-Style API: Familiar patterns for .NET developers
- 🔍 LINQ Queries: Type-safe querying with method chaining
- 🏗️ Entity-based Queries: GORM-style struct patterns with comparison operators ✨
- 📊 Enhanced Aggregations: Entity-based Sum, Average, Min, Max operations ✨
- 🔗 Include/Select Support: Load relationships and specific fields
- 📈 Change Tracking: Automatic entity change detection
- 🔄 Migrations: Code-first database migrations with Go files
- 💾 DbSets: Type-safe entity collections with generics
- 🗃️ Multiple Databases: PostgreSQL, MySQL, SQLite support
- 🐘 PostgreSQL Pascal Case: Automatic field name translation with quoted identifiers
- 🚀 Zero Configuration: Automatic database-specific optimizations
- ⚡ Field Validation: Runtime validation with clear error messages
- 🔢 Comparison Operators: Support for
>
,<
,>=
,<=
,!=
in all query patterns ✨
go get github.com/shepherrrd/gontext
package main
import (
"github.com/google/uuid"
"github.com/shepherrrd/gontext"
)
// Define your entities
type User struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
Username string `gorm:"uniqueIndex;not null"`
Email string `gorm:"uniqueIndex;not null"`
Name string `gorm:"not null"`
}
// Create your DbContext
type AppContext struct {
*gontext.DbContext
Users *gontext.LinqDbSet[User]
}
func NewAppContext(connectionString string) (*AppContext, error) {
ctx, err := gontext.NewDbContext(connectionString, "postgres")
if err != nil {
return nil, err
}
users := gontext.RegisterEntity[User](ctx)
return &AppContext{
DbContext: ctx,
Users: users,
}, nil
}
GoNtext automatically handles PostgreSQL case-sensitive identifiers! No manual configuration required.
When using PostgreSQL, GoNtext automatically:
- Uses struct names as table names:
User
struct →"User"
table (notusers
) - Uses field names as column names:
Username
field →"Username"
column - Quotes all identifiers: Proper PostgreSQL case-sensitive handling
- Translates all queries: WHERE, ORDER BY, SELECT - everything works automatically
type User struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
Username string `gorm:"uniqueIndex;not null"`
Email string `gorm:"uniqueIndex;not null"`
PasswordHash string `gorm:"not null"`
IsActive bool `gorm:"not null;default:true"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
// Setup - Zero configuration needed!
ctx, _ := gontext.NewDbContext("postgres://user:pass@localhost/db", "postgres")
userSet := gontext.RegisterEntity[User](ctx)
// All queries automatically use quoted PostgreSQL identifiers:
user, _ := userSet.WhereField("Username", "john").FirstOrDefault()
// SQL: SELECT * FROM "User" WHERE "Username" = 'john'
users, _ := userSet.OrderByField("Email").ToList()
// SQL: SELECT * FROM "User" ORDER BY "Email" ASC
userSet.WhereField("IsActive", true).Delete()
// SQL: DELETE FROM "User" WHERE "IsActive" = true
- ✅ Table Names:
User
struct becomes"User"
table (Pascal case) - ✅ Column Names:
Username
field becomes"Username"
column (Pascal case) - ✅ All Query Types: INSERT, SELECT, UPDATE, DELETE - all automatically translated
- ✅ Complex Queries: WHERE with AND/OR/parentheses, LIKE, IN - all supported
- ✅ Comparison Operators: Support for
>
,<
,>=
,<=
,!=
in Where conditions ✨ - ✅ Zero Boilerplate: No
TableName()
methods needed, no manual quoting
OLD WAY (not needed anymore):
func (User) TableName() string {
return "User" // ❌ Don't do this anymore!
}
NEW WAY (automatic):
type User struct {
// Just define your struct - GoNtext handles the rest! ✅
Username string
Email string
}
GoNtext automatically uses the struct name (User
) as the table name with proper PostgreSQL quoting.
GoNtext supports multiple query patterns for maximum flexibility!
// Age comparisons
users, _ := ctx.Users.Where("Age", ">18").ToList() // Age > 18
users, _ := ctx.Users.Where("Age", ">=21").ToList() // Age >= 21
users, _ := ctx.Users.Where("Age", "<65").ToList() // Age < 65
users, _ := ctx.Users.Where("Age", "<=30").ToList() // Age <= 30
users, _ := ctx.Users.Where("Age", "!=25").ToList() // Age != 25
// String comparisons
users, _ := ctx.Users.Where("Username", "!=admin").ToList() // Username != 'admin'
// Numeric fields
files, _ := ctx.Files.Where("Size", ">1048576").ToList() // Size > 1MB
// Static typing with entity structs
user, _ := ctx.Users.Where(&User{Email: "[email protected]"}).FirstOrDefault()
// Entity-based queries with comparison operators ✨ NEW!
users, _ := ctx.Users.Where(&User{Age: ">18"}).ToList() // Age > 18
files, _ := ctx.Files.Where(&File{Size: ">=1048576"}).ToList() // Size >= 1MB
users, _ := ctx.Users.Where(&User{Username: "!=admin"}).ToList() // Username != 'admin'
// Combined entity patterns
activeAdults, _ := ctx.Users.Where(&User{IsActive: true, Age: ">=18"}).ToList()
// Field-based OR with operators
users, _ := ctx.Users.Where("Age", ">=18").Or("Role", "admin").ToList()
users, _ := ctx.Users.Where("Status", "!=inactive").Or("Priority", ">5").ToList()
// Entity-based OR operations ✨ NEW!
users, _ := ctx.Users.Where(&User{Role: "admin"}).Or(&User{Age: ">=65"}).ToList()
users, _ := ctx.Users.Where(&User{Email: "[email protected]"}).Or(&User{Username: "admin"}).ToList()
// Mixed pattern OR operations
users, _ := ctx.Users.Where("IsActive", true).Or(&User{Role: "admin"}).ToList()
GoNtext provides multiple patterns for aggregations and ordering!
// Entity-based Sum - specify field using entity pattern
totalSize, _ := ctx.Files.Sum(&File{Size: 0}) // Sum file sizes
totalRevenue, _ := ctx.Orders.Sum(&Order{Amount: 0.0}) // Sum order amounts
// Entity-based Average
avgAge, _ := ctx.Users.Average(&User{Age: 0}) // Average user age
avgRating, _ := ctx.Products.Average(&Product{Rating: 0.0}) // Average product rating
// Entity-based Min/Max
minPrice, _ := ctx.Products.Min(&Product{Price: 0.0}) // Minimum price
maxScore, _ := ctx.GameResults.Max(&GameResult{Score: 0}) // Maximum score
// Chain with WHERE conditions
totalAdultAge, _ := ctx.Users.Where("Age", ">=18").Sum(&User{Age: 0})
avgActiveUserAge, _ := ctx.Users.Where(&User{IsActive: true}).Average(&User{Age: 0})
// Field name aggregations (backward compatible)
totalSize, _ := ctx.Files.SumField("Size")
avgAge, _ := ctx.Users.AverageField("Age")
minPrice, _ := ctx.Products.MinField("Price")
maxScore, _ := ctx.GameResults.MaxField("Score")
// With comparison operators
expensiveItemsTotal, _ := ctx.Products.Where("Price", ">100").SumField("Price")
youngUsersAvgAge, _ := ctx.Users.Where("Age", "<25").AverageField("Age")
// Multiple ordering patterns
users, _ := ctx.Users.OrderBy("Name").ToList() // String field name
users, _ := ctx.Users.OrderBy(func(u User) interface{} { // Function selector
return u.CreatedAt
}).ToList()
// Descending order
users, _ := ctx.Users.OrderByDescending("Age").ToList() // String field name
users, _ := ctx.Users.OrderByDescending(func(u User) interface{} { // Function selector
return u.LastLogin
}).ToList()
// Entity-based ordering (uses first non-zero field)
users, _ := ctx.Users.OrderByAscending(&User{Name: "placeholder"}).ToList()
users, _ := ctx.Users.OrderByDescendingEntity(&User{CreatedAt: time.Now()}).ToList()
// Chain with other operations
topSpenders, _ := ctx.Users.
Where("IsActive", true).
OrderByDescending("TotalSpent").
Take(10).
ToList()
// E-commerce analytics
type Product struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Price float64 `gorm:"not null"`
Rating float64 `gorm:"default:0"`
InStock bool `gorm:"default:true"`
CategoryID uint `gorm:"not null"`
}
// Get total inventory value for in-stock premium products
inventoryValue, _ := ctx.Products.
Where(&Product{InStock: true}).
Where("Price", ">50").
Sum(&Product{Price: 0.0})
// Find average rating of highly-rated products
avgRating, _ := ctx.Products.
Where("Rating", ">=4.0").
Average(&Product{Rating: 0.0})
// Get top 5 most expensive products by category
topProducts, _ := ctx.Products.
Where(&Product{CategoryID: 1}).
OrderByDescending("Price").
Take(5).
ToList()
// Complex aggregation with OR conditions
totalValue, _ := ctx.Products.
Where("Rating", ">=4.5").
Or(&Product{InStock: true}).
Sum(&Product{Price: 0.0})
GoNtext provides EF Core-style Include and Select functionality with type safety!
// Include single relationship
users, _ := ctx.Users.Include("Posts").ToList()
// Include multiple relationships
users, _ := ctx.Users.Include("Posts", "Profile").ToList()
// Auto-include all relationships
users, _ := ctx.Users.IncludeAll().ToList()
// Chain with other operations
users, _ := ctx.Users.Include("Posts").Where("IsActive", true).ToList()
// Select only specific fields
users, _ := ctx.Users.Select("ID", "Username", "Email").ToList()
// Exclude sensitive fields
users, _ := ctx.Users.Omit("PasswordHash").ToList()
// Combine Include and Select
users, _ := ctx.Users.Include("Posts").Select("ID", "Username").ToList()
// ✅ Valid - compiles and runs
users, _ := ctx.Users.Include("Posts").ToList()
// ❌ Invalid - panics with: Field 'Post' not found on User
users, _ := ctx.Users.Include("Post").ToList() // Typo in field name
GoNtext validates field names at runtime and provides clear error messages for fast debugging.
// Load users with their posts and profiles, excluding sensitive data
results, _ := ctx.Users.
Include("Posts", "Profile").
Omit("PasswordHash").
Where("IsActive", true).
OrderBy("Username").
Skip(0).Take(10).
ToList()
// Auto-detect and load all relationships
users, _ := ctx.Users.IncludeAll().ToList()
You can override the default table naming by implementing the TableName()
method:
type User struct {
Id uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
Username string `gorm:"uniqueIndex;not null"`
Email string `gorm:"uniqueIndex;not null"`
}
// Custom table name - overrides default "User"
func (User) TableName() string {
return "app_users" // Will create "app_users" table instead of "User"
}
type Product struct {
Id uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
Name string `gorm:"not null"`
}
// No TableName() method - uses default "Product" table name
GoNtext respects the TableName()
method across all operations:
- ✅ CRUD Operations:
ctx.Users.Add()
,ctx.Users.Find()
, etc. use custom table name - ✅ LINQ Queries:
ctx.Users.WhereField().ToList()
uses custom table name - ✅ Migrations: Migration files generate SQL for custom table names
- ✅ PostgreSQL: Automatically quotes custom table names:
"app_users"
// Setup with custom table names
ctx, _ := gontext.NewDbContext("postgres://user:pass@localhost/db", "postgres")
userSet := gontext.RegisterEntity[User](ctx) // Uses "app_users" table
productSet := gontext.RegisterEntity[Product](ctx) // Uses "Product" table
// All operations use the custom table names automatically
user, _ := userSet.WhereField("Username", "john").FirstOrDefault()
// SQL: SELECT * FROM "app_users" WHERE "Username" = 'john'
product, _ := productSet.WhereField("Name", "laptop").FirstOrDefault()
// SQL: SELECT * FROM "Product" WHERE "Name" = 'laptop'
- Legacy databases: Match existing table names
- Naming conventions: Follow company/team standards (e.g.,
tbl_users
,app_users
) - Multi-tenant: Different table prefixes per tenant
- Database conventions: Snake_case, plural names, etc.
GoNtext now supports GORM-style static typing with struct patterns! Use familiar GORM syntax alongside EF Core-style LINQ methods.
- 🔍 Struct-based Where Clauses: Use
&User{Email: "[email protected]"}
instead of strings - 🚀 Multiple Query Patterns: Support for SQL, field names, and struct patterns
- 🔗 OR Operations: Chain WHERE and OR conditions with struct patterns
- ⚡ GORM-Compatible: Drop-in replacement for common GORM operations
- 🎯 Type-Safe: Compile-time checking with struct patterns
GoNtext supports 3 query patterns:
// Pattern 1: SQL with parameters (traditional)
user, _ := ctx.Users.Where("Email = ?", "[email protected]").FirstOrDefault()
// Pattern 2: Field name with value (EF Core style)
user, _ := ctx.Users.Where("Email", "[email protected]").FirstOrDefault()
// Pattern 3: Struct pattern (GORM style) ✨ NEW!
user, _ := ctx.Users.Where(&entities.User{Email: "[email protected]"}).FirstOrDefault()
Use OR conditions with struct patterns for complex queries:
// Login with email OR username (like GORM)
user, _ := ctx.Users.Where(&entities.User{Email: "[email protected]"}).
OrField("Username", "john").
FirstOrDefault()
// Multiple OR conditions with structs
users, _ := ctx.Users.Where(&entities.User{Role: "admin"}).
OrEntity(entities.User{Role: "manager"}).
ToList()
// Mixed patterns
user, _ := ctx.Users.Where("IsActive", true).
OrField("Role", "admin").
FirstOrDefault()
Use familiar GORM patterns with GoNtext's change tracking:
// Create (GORM style)
user := &entities.User{Username: "john", Email: "[email protected]"}
err := ctx.Users.Create(user)
// Save (creates or updates like GORM)
user.Email = "[email protected]"
err := ctx.Users.Save(user)
// First with struct pattern (like GORM)
user, err := ctx.Users.First(&entities.User{Email: "[email protected]"})
// Update with struct pattern
user.Username = "newusername"
err := ctx.Users.Update(*user)
// Find by ID (GORM style)
user, err := ctx.Users.Find(userID)
Perfect for authentication with email OR username:
func LoginUser(emailOrUsername, password string) (*User, error) {
// Search by email OR username using static typing
user, err := ctx.Users.WhereField("email", emailOrUsername).
OrField("username", emailOrUsername).
FirstOrDefault()
if err != nil || user == nil {
return nil, fmt.Errorf("invalid credentials")
}
// Verify password...
return user, nil
}
- Zero Runtime Overhead: Struct patterns compile to optimized SQL
- PostgreSQL Optimized: Automatic field name translation with quoted identifiers
- GORM Compatible: Familiar methods work the same way
- Change Tracking: Automatic entity state management like EF Core
Choose what you want to learn:
Learn the fundamentals:
- Creating entities
- Saving changes
- Querying data
- Updates and deletes
Database schema management:
- Creating migrations
- Model snapshots
- Schema evolution
- Database updates
Advanced querying:
- Where conditions
- Ordering and pagination
- String operations
- Aggregations
- Method chaining
The built-in CLI has limitations. For proper migrations, you need to set up entity registration:
// File: migrations_context.go
func CreateDesignTimeContext() (*gontext.DbContext, error) {
ctx, err := gontext.NewDbContext("your-db-url", "postgres")
if err != nil {
return nil, err
}
// Register ALL your entities
gontext.RegisterEntity[User](ctx)
gontext.RegisterEntity[Post](ctx)
// ... register every entity
return ctx, nil
}
// Add to your main.go
func handleMigrations() {
if len(os.Args) > 1 && os.Args[1] == "migrate:add" {
// Use your design-time context for migrations
ctx, _ := CreateDesignTimeContext()
// Generate migration files
}
}
See Migrations Example for complete setup.
- 🎯 Familiar: Uses EF Core patterns you already know
- 🔍 Type-Safe: LINQ queries with compile-time checking
- 🚀 Powerful: Full SQL capabilities via GORM integration
- 📦 Complete: Migrations, change tracking, and DbContext patterns
- 🔄 Flexible: Use LINQ or drop down to raw SQL when needed
GoNtext brings the best of Entity Framework Core to Go!
EF Core (C#) | GoNtext (Go) |
---|---|
context.Users.Add(user) |
ctx.Users.Add(user) |
context.SaveChanges() |
ctx.SaveChanges() |
context.Users.Where(x => x.IsActive).ToList() |
ctx.Users.Where("IsActive", true).ToList() |
context.Users.Where(x => x.IsActive).ToList() |
ctx.Users.Where(&User{IsActive: true}).ToList() ✨ |
context.Users.FirstOrDefault(x => x.Id == id) |
ctx.Users.ById(id) |
context.Users.FirstOrDefault(x => x.Id == id) |
ctx.Users.Where(&User{Id: id}).FirstOrDefault() ✨ |
context.Users.OrderBy(x => x.Email) |
ctx.Users.OrderBy("Email") |
context.Users.Where(x => x.Age > 18) |
ctx.Users.Where("Age", ">18") ✨ |
context.Users.Where(x => x.Age > 18) |
ctx.Users.Where(&User{Age: ">18"}) ✨ NEW! |
context.Orders.Sum(x => x.Amount) |
ctx.Orders.Sum(&Order{Amount: 0.0}) ✨ NEW! |
context.Users.Average(x => x.Age) |
ctx.Users.Average(&User{Age: 0}) ✨ NEW! |
Pascal case tables (Users ) |
Pascal case tables ("User" ) ✨ |
Pascal case columns (IsActive ) |
Pascal case columns ("IsActive" ) ✨ |
[Table("app_users")] class User |
func (User) TableName() string { return "app_users" } ✨ |
Traditional GORM (Go) | GoNtext (Go) |
---|---|
db.Where(&User{Email: "test"}).First(&user) |
ctx.Users.Where(&User{Email: "test"}).FirstOrDefault() ✨ |
db.Where("email = ?", email).Or("username = ?", username).First(&user) |
ctx.Users.Where("Email", email).Or("Username", username).FirstOrDefault() ✨ |
db.Where("age > ?", 18).Find(&users) |
ctx.Users.Where("Age", ">18").ToList() ✨ |
db.Where("age > ?", 18).Find(&users) |
ctx.Users.Where(&User{Age: ">18"}).ToList() ✨ NEW! |
db.Create(&user) |
ctx.Users.Add(user) ✨ (EF Core style) |
db.Save(&user) |
ctx.Users.Save(&user) ✨ |
db.First(&user, id) |
ctx.Users.Find(id) ✨ |
db.Model(&Order{}).Select("SUM(amount)").Scan(&total) |
ctx.Orders.Sum(&Order{Amount: 0.0}) ✨ NEW! |
db.Model(&User{}).Select("AVG(age)").Scan(&avg) |
ctx.Users.Average(&User{Age: 0}) ✨ NEW! |
Manual change tracking | Automatic change tracking ✨ |
Manual migrations | Code-first migrations ✨ |
Snake_case by default | Pascal case with PostgreSQL ✨ |
GoNtext combines the best features:
- 🎯 EF Core: Change tracking, DbContext patterns, LINQ-style queries
- ⚡ GORM: Familiar syntax, struct-based queries, flexible operations
- 🐘 PostgreSQL: Automatic Pascal case with quoted identifiers
- 🚀 Performance: Zero runtime overhead, optimized SQL generation
- 📚 Complete API Reference - Full documentation of all GoNtext LINQ methods
- CRUD Operations Guide
- Migrations Setup Guide
- LINQ Queries Guide
# Clone and test
git clone https://github.com/shepherrrd/gontext
cd gontext/examples/01-crud
go mod tidy
createdb test_gontext
go run .