Marten
The implementation of database sessions using Marten provides configuration and registration for integrating with the Marten document database. Marten uses PostgreSQL as a document database and event store for .NET applications.
To add Raiqub.Expressions.Marten library to a .NET project, go get it from Nuget:
dotnet add package Raiqub.Expressions.MartenPM> Install-Package Raiqub.Expressions.Martenpaket add nuget Raiqub.Expressions.MartenPrerequisites
Before using Raiqub.Expressions.Marten, ensure you have Marten configured in your project. Add the Marten package:
dotnet add package MartenMarten Configuration
First, configure Marten with your PostgreSQL connection string and document mappings:
services.AddMarten(options =>
{
// Configure connection string
options.Connection("Host=localhost;Database=myapp;Username=postgres;Password=password");
// Configure document mappings
options.Schema.For<Product>().Identity(x => x.Id);
options.Schema.For<Order>().Identity(x => x.Id);
options.Schema.For<Customer>().Identity(x => x.Id);
// Optional: Configure JSON serialization
options.UseDefaultSerialization(
serializerType: SerializerType.SystemTextJson,
enumStorage: EnumStorage.AsString);
});Register Database Session
After configuring Marten, register the session and session factories using the following extension method:
services.AddMartenExpressions()
.AddSingleContext();This registers:
IDbQuerySessionFactory- For creating read-only query sessionsIDbSessionFactory- For creating read/write sessionsIDbQuerySession- For querying documentsIDbSession- For querying and modifying documents
Basic Usage
Querying Documents
Use specifications and query strategies with Marten just like with Entity Framework Core:
public class ProductService
{
private readonly IDbQuerySession _session;
public ProductService(IDbQuerySession session)
{
_session = session;
}
public async Task<IReadOnlyList<Product>> GetInStockProductsAsync(
CancellationToken cancellationToken = default)
{
var query = _session.Query(ProductSpecification.IsInStock);
return await query.ToListAsync(cancellationToken);
}
public async Task<PagedResult<Product>> GetProductsPagedAsync(
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
var query = _session.Query<Product>();
return await query.ToPagedListAsync(pageNumber, pageSize, cancellationToken);
}
}Persisting Documents
Use the write session to add, update, or delete documents:
public class ProductService
{
private readonly IDbSession _session;
public ProductService(IDbSession session)
{
_session = session;
}
public async Task CreateProductAsync(Product product, CancellationToken cancellationToken = default)
{
_session.Add(product);
await _session.SaveChangesAsync(cancellationToken);
}
public async Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default)
{
_session.Update(product);
await _session.SaveChangesAsync(cancellationToken);
}
public async Task DeleteProductAsync(Product product, CancellationToken cancellationToken = default)
{
_session.Remove(product);
await _session.SaveChangesAsync(cancellationToken);
}
}Query Strategies with Marten
Query strategies work seamlessly with Marten's LINQ provider:
public class GetActiveCustomersQueryStrategy : EntityQueryStrategy<Customer, CustomerDto>
{
protected override IQueryable<CustomerDto> ExecuteCore(IQueryable<Customer> source)
{
return source
.Where(c => c.IsActive)
.OrderBy(c => c.LastName)
.ThenBy(c => c.FirstName)
.Select(c => new CustomerDto
{
Id = c.Id,
FullName = $"{c.FirstName} {c.LastName}",
Email = c.Email
});
}
}
// Usage
await using var session = sessionFactory.Create();
var query = session.Query(new GetActiveCustomersQueryStrategy());
var customers = await query.ToListAsync();Marten-Specific Features
Compiled Queries
Marten supports compiled queries for improved performance. While Raiqub.Expressions doesn't directly expose this, you can create optimized query strategies:
public static class ProductQueryStrategy
{
public static IEntityQueryStrategy<Product, Product> GetByCategory(string category) =>
QueryStrategy.CreateForEntity(
(IQueryable<Product> source) => source
.Where(p => p.Category == category)
.OrderBy(p => p.Name));
}Full-Text Search
Marten provides full-text search capabilities that you can use in query strategies:
public class SearchProductsByTextQueryStrategy : EntityQueryStrategy<Product, Product>
{
private readonly string _searchTerm;
public SearchProductsByTextQueryStrategy(string searchTerm)
{
_searchTerm = searchTerm;
}
protected override IQueryable<Product> ExecuteCore(IQueryable<Product> source)
{
// Marten's PlainTextSearch extension method
return source.Where(x => x.Search(_searchTerm));
}
}Soft Deletes
If you configure soft deletes in Marten, you can create specifications to filter them:
public static class ProductSpecification
{
public static Specification<Product> IsNotDeleted { get; } =
Specification.Create<Product>(p => !p.IsDeleted || p.Deleted == null);
public static Specification<Product> IsActive { get; } =
IsNotDeleted.And(Specification.Create<Product>(p => p.IsActive));
}Include Related Documents
Marten doesn't use navigation properties like EF Core, but you can load related documents:
public class GetOrdersWithCustomerQueryStrategy : IQueryStrategy<OrderWithCustomer>
{
public IQueryable<OrderWithCustomer> Execute(IQuerySource source)
{
var orders = source.GetSet<Order>();
var customers = source.GetSet<Customer>();
return from order in orders
join customer in customers on order.CustomerId equals customer.Id
select new OrderWithCustomer
{
OrderId = order.Id,
OrderDate = order.Date,
CustomerName = customer.Name,
TotalAmount = order.TotalAmount
};
}
}Advanced Configuration
Optimistic Concurrency
Marten supports optimistic concurrency. Configure it in your document mappings:
services.AddMarten(options =>
{
options.Connection(connectionString);
options.Schema.For<Product>()
.Identity(x => x.Id)
.UseOptimisticConcurrency(true);
});Custom Schema
Configure custom database schema for your documents:
services.AddMarten(options =>
{
options.Connection(connectionString);
// Use custom schema instead of default 'public'
options.DatabaseSchemaName = "myapp";
options.Schema.For<Product>().DatabaseSchemaName("catalog");
options.Schema.For<Order>().DatabaseSchemaName("sales");
});Indexing
Add indexes to improve query performance:
services.AddMarten(options =>
{
options.Connection(connectionString);
options.Schema.For<Product>()
.Identity(x => x.Id)
.Index(x => x.Category)
.Index(x => x.Price)
.Index(x => x.CreatedAt);
});Performance Tips
Use Projections
Always project to DTOs to reduce the amount of data transferred:
// Good - Only select needed fields
public class GetProductSummaryQueryStrategy : EntityQueryStrategy<Product, ProductSummary>
{
protected override IQueryable<ProductSummary> ExecuteCore(IQueryable<Product> source)
{
return source.Select(p => new ProductSummary
{
Id = p.Id,
Name = p.Name,
Price = p.Price
});
}
}Batch Operations
Use batch operations when inserting or updating multiple documents:
public async Task CreateProductsAsync(
IEnumerable<Product> products,
CancellationToken cancellationToken = default)
{
foreach (var product in products)
{
_session.Add(product);
}
// Single SaveChanges for all products
await _session.SaveChangesAsync(cancellationToken);
}Streaming Large Result Sets
For large result sets, use ToAsyncEnumerable to stream results:
public async Task ProcessAllProductsAsync(CancellationToken cancellationToken = default)
{
var query = _session.Query<Product>();
await foreach (var product in query.ToAsyncEnumerable(cancellationToken))
{
// Process one product at a time
await ProcessProductAsync(product, cancellationToken);
}
}Differences from Entity Framework Core
Key differences when using Marten vs EF Core:
- Document Model: Marten stores documents as JSON, not relational tables
- No Foreign Keys: Relationships are managed in application code
- No Migrations: Schema changes are applied automatically
- Event Sourcing: Marten has built-in event sourcing capabilities
- Full-Text Search: Native PostgreSQL full-text search support
- Performance: Generally faster for read-heavy workloads
Troubleshooting
Connection Issues
Ensure your PostgreSQL server is running and accessible:
services.AddMarten(options =>
{
options.Connection("Host=localhost;Port=5432;Database=myapp;Username=postgres;Password=password");
// Enable detailed logging
options.Logger(new ConsoleMartenLogger());
});Schema Updates
If you modify document structure, Marten can update the schema automatically:
// In development
var store = services.BuildServiceProvider().GetRequiredService<IDocumentStore>();
await store.Schema.ApplyAllConfiguredChangesToDatabaseAsync();For more information, refer to the official Marten documentation.