Convey.Persistence.MongoDB
MongoDB integration package providing repository patterns, database initialization, seeding capabilities, and advanced MongoDB features for .NET applications.
Installation
dotnet add package Convey.Persistence.MongoDB
Overview
Convey.Persistence.MongoDB provides:
- Repository pattern - Generic repository with CRUD operations
- Database initialization - Automatic database and collection setup
- Data seeding - Initial data population capabilities
- Pagination support - Built-in paged query results
- Session management - MongoDB session factory for transactions
- Convention registration - Automatic BSON serialization conventions
- Integration testing support - Random database suffix for tests
Configuration
Basic Setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddConvey()
.AddMongo();
var app = builder.Build();
app.Run();
MongoDB Options
Configure in appsettings.json:
{
"mongo": {
"connectionString": "mongodb://localhost:27017",
"database": "myapp",
"seed": false,
"setRandomDatabaseSuffix": false
}
}
Advanced Configuration
builder.Services.AddConvey()
.AddMongo(x => x
.WithConnectionString("mongodb://localhost:27017")
.WithDatabase("myapp")
.WithSeed(true)
.WithRandomDatabaseSuffix(true) // For testing
);
Key Features
1. Entity Models
Define entities that implement IIdentifiable<T>:
public class User : IIdentifiable<Guid>
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Role { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsActive { get; set; } = true;
public User()
{
Id = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
}
public User(string name, string email, string role = "User") : this()
{
Name = name;
Email = email;
Role = role;
}
}
public class Product : IIdentifiable<string>
{
public string Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
public string Description { get; set; }
public int Stock { get; set; }
public DateTime CreatedAt { get; set; }
public Product()
{
Id = ObjectId.GenerateNewId().ToString();
CreatedAt = DateTime.UtcNow;
}
}
2. Repository Pattern
Use the generic repository for data access:
public interface IUserRepository : IMongoRepository<User, Guid>
{
Task<User> GetByEmailAsync(string email);
Task<IReadOnlyList<User>> GetActiveUsersAsync();
Task<PagedResult<User>> BrowseByRoleAsync(string role, IPagedQuery query);
}
public class UserRepository : IUserRepository
{
private readonly IMongoRepository<User, Guid> _repository;
public UserRepository(IMongoRepository<User, Guid> repository)
{
_repository = repository;
}
public IMongoCollection<User> Collection => _repository.Collection;
public Task<User> GetAsync(Guid id) => _repository.GetAsync(id);
public Task<User> GetAsync(Expression<Func<User, bool>> predicate) =>
_repository.GetAsync(predicate);
public Task<IReadOnlyList<User>> FindAsync(Expression<Func<User, bool>> predicate) =>
_repository.FindAsync(predicate);
public Task<PagedResult<User>> BrowseAsync<TQuery>(Expression<Func<User, bool>> predicate, TQuery query)
where TQuery : IPagedQuery => _repository.BrowseAsync(predicate, query);
public Task AddAsync(User entity) => _repository.AddAsync(entity);
public Task UpdateAsync(User entity) => _repository.UpdateAsync(entity);
public Task UpdateAsync(User entity, Expression<Func<User, bool>> predicate) =>
_repository.UpdateAsync(entity, predicate);
public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id);
public Task DeleteAsync(Expression<Func<User, bool>> predicate) =>
_repository.DeleteAsync(predicate);
public Task<bool> ExistsAsync(Expression<Func<User, bool>> predicate) =>
_repository.ExistsAsync(predicate);
// Custom methods
public Task<User> GetByEmailAsync(string email) =>
_repository.GetAsync(x => x.Email == email);
public Task<IReadOnlyList<User>> GetActiveUsersAsync() =>
_repository.FindAsync(x => x.IsActive);
public Task<PagedResult<User>> BrowseByRoleAsync(string role, IPagedQuery query) =>
_repository.BrowseAsync(x => x.Role == role, query);
}
// Register the repository
builder.Services.AddScoped<IUserRepository, UserRepository>();
3. Service Layer Implementation
public class UserService
{
private readonly IUserRepository _userRepository;
private readonly ILogger<UserService> _logger;
public UserService(IUserRepository userRepository, ILogger<UserService> logger)
{
_userRepository = userRepository;
_logger = logger;
}
public async Task<User> GetByIdAsync(Guid id)
{
return await _userRepository.GetAsync(id);
}
public async Task<User> GetByEmailAsync(string email)
{
return await _userRepository.GetByEmailAsync(email);
}
public async Task<PagedResult<User>> BrowseAsync(BrowseUsersQuery query)
{
Expression<Func<User, bool>> predicate = x => x.IsActive;
if (!string.IsNullOrWhiteSpace(query.Role))
{
predicate = predicate.And(x => x.Role == query.Role);
}
if (!string.IsNullOrWhiteSpace(query.Search))
{
predicate = predicate.And(x =>
x.Name.Contains(query.Search) ||
x.Email.Contains(query.Search));
}
return await _userRepository.BrowseAsync(predicate, query);
}
public async Task<User> CreateAsync(CreateUserCommand command)
{
var existingUser = await _userRepository.GetByEmailAsync(command.Email);
if (existingUser != null)
{
throw new UserAlreadyExistsException(command.Email);
}
var user = new User(command.Name, command.Email, command.Role);
await _userRepository.AddAsync(user);
_logger.LogInformation("User {UserId} created with email {Email}", user.Id, user.Email);
return user;
}
public async Task UpdateAsync(Guid id, UpdateUserCommand command)
{
var user = await _userRepository.GetAsync(id);
if (user == null)
{
throw new UserNotFoundException(id);
}
user.Name = command.Name;
user.Email = command.Email;
user.UpdatedAt = DateTime.UtcNow;
await _userRepository.UpdateAsync(user);
_logger.LogInformation("User {UserId} updated", id);
}
public async Task DeleteAsync(Guid id)
{
var user = await _userRepository.GetAsync(id);
if (user == null)
{
throw new UserNotFoundException(id);
}
await _userRepository.DeleteAsync(id);
_logger.LogInformation("User {UserId} deleted", id);
}
}
4. Data Seeding
Implement automatic data seeding:
public class UserSeeder : IMongoDbSeeder
{
private readonly IUserRepository _userRepository;
public UserSeeder(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task SeedAsync()
{
if (await _userRepository.ExistsAsync(x => true))
{
return; // Already seeded
}
var users = new[]
{
new User("Admin User", "admin@example.com", "Admin"),
new User("John Doe", "john@example.com", "User"),
new User("Jane Smith", "jane@example.com", "User"),
new User("Manager User", "manager@example.com", "Manager")
};
foreach (var user in users)
{
await _userRepository.AddAsync(user);
}
}
}
// Register seeder
builder.Services.AddConvey()
.AddMongo(seederType: typeof(UserSeeder));
5. Pagination Queries
Implement paginated queries:
public class BrowseUsersQuery : PagedQueryBase
{
public string Role { get; set; }
public string Search { get; set; }
public bool? IsActive { get; set; }
}
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public async Task<ActionResult<PagedResult<UserDto>>> BrowseAsync([FromQuery] BrowseUsersQuery query)
{
var result = await _userService.BrowseAsync(query);
return Ok(new PagedResult<UserDto>
{
Items = result.Items.Select(UserDto.FromUser).ToList(),
Page = result.Page,
PageSize = result.PageSize,
TotalItems = result.TotalItems,
TotalPages = result.TotalPages
});
}
}
Advanced Features
1. Transactions with Sessions
public class OrderService
{
private readonly IMongoSessionFactory _sessionFactory;
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
public OrderService(
IMongoSessionFactory sessionFactory,
IOrderRepository orderRepository,
IProductRepository productRepository)
{
_sessionFactory = sessionFactory;
_orderRepository = orderRepository;
_productRepository = productRepository;
}
public async Task<Order> CreateOrderAsync(CreateOrderCommand command)
{
using var session = await _sessionFactory.CreateAsync();
session.StartTransaction();
try
{
// Check product availability
var product = await _productRepository.GetAsync(command.ProductId);
if (product.Stock < command.Quantity)
{
throw new InsufficientStockException(command.ProductId);
}
// Update product stock
product.Stock -= command.Quantity;
await _productRepository.UpdateAsync(product);
// Create order
var order = new Order(command.CustomerId, command.ProductId, command.Quantity, product.Price);
await _orderRepository.AddAsync(order);
await session.CommitTransactionAsync();
return order;
}
catch
{
await session.AbortTransactionAsync();
throw;
}
}
}
2. Custom Repository Implementation
public class CustomUserRepository : MongoRepository<User, Guid>, IUserRepository
{
public CustomUserRepository(IMongoDatabase database) : base(database, "users")
{
}
public async Task<User> GetByEmailAsync(string email)
{
return await Collection
.Find(x => x.Email == email && x.IsActive)
.FirstOrDefaultAsync();
}
public async Task<IReadOnlyList<User>> GetUsersByRoleAsync(string role)
{
return await Collection
.Find(x => x.Role == role && x.IsActive)
.SortBy(x => x.Name)
.ToListAsync();
}
public async Task<long> GetUserCountByRoleAsync(string role)
{
return await Collection.CountDocumentsAsync(x => x.Role == role && x.IsActive);
}
public async Task BulkUpdateUserRoleAsync(IEnumerable<Guid> userIds, string newRole)
{
var filter = Builders<User>.Filter.In(x => x.Id, userIds);
var update = Builders<User>.Update
.Set(x => x.Role, newRole)
.Set(x => x.UpdatedAt, DateTime.UtcNow);
await Collection.UpdateManyAsync(filter, update);
}
}
3. Database Initialization
public class DatabaseInitializer : IMongoDbInitializer
{
private readonly IMongoDatabase _database;
public DatabaseInitializer(IMongoDatabase database)
{
_database = database;
}
public async Task InitializeAsync()
{
// Create indexes
var userCollection = _database.GetCollection<User>("users");
await userCollection.Indexes.CreateOneAsync(
new CreateIndexModel<User>(
Builders<User>.IndexKeys.Ascending(x => x.Email),
new CreateIndexOptions { Unique = true }
)
);
await userCollection.Indexes.CreateOneAsync(
new CreateIndexModel<User>(
Builders<User>.IndexKeys.Ascending(x => x.Role)
)
);
// Create other collections and indexes
var productCollection = _database.GetCollection<Product>("products");
await productCollection.Indexes.CreateOneAsync(
new CreateIndexModel<Product>(
Builders<Product>.IndexKeys.Text(x => x.Name).Text(x => x.Description)
)
);
}
}
// Register initializer
builder.Services.AddSingleton<IMongoDbInitializer, DatabaseInitializer>();
API Reference
IMongoRepository<TEntity, TIdentifiable>
public interface IMongoRepository<TEntity, in TIdentifiable> where TEntity : IIdentifiable<TIdentifiable>
{
IMongoCollection<TEntity> Collection { get; }
Task<TEntity> GetAsync(TIdentifiable id);
Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate);
Task<IReadOnlyList<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate);
Task<PagedResult<TEntity>> BrowseAsync<TQuery>(Expression<Func<TEntity, bool>> predicate, TQuery query) where TQuery : IPagedQuery;
Task AddAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task UpdateAsync(TEntity entity, Expression<Func<TEntity, bool>> predicate);
Task DeleteAsync(TIdentifiable id);
Task DeleteAsync(Expression<Func<TEntity, bool>> predicate);
Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate);
}
Extension Methods
AddMongo()
public static IConveyBuilder AddMongo(
this IConveyBuilder builder,
string sectionName = "mongo",
Type seederType = null,
bool registerConventions = true)
Registers MongoDB services with configuration from appsettings.
AddMongo() with Options Builder
public static IConveyBuilder AddMongo(
this IConveyBuilder builder,
Func<IMongoDbOptionsBuilder, IMongoDbOptionsBuilder> buildOptions,
Type seederType = null,
bool registerConventions = true)
Registers MongoDB services with fluent configuration.
Complete Example
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddConvey()
.AddMongo(seederType: typeof(DataSeeder))
.AddCommandHandlers()
.AddQueryHandlers();
// Register repositories and services
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
// Entity
public class User : IIdentifiable<Guid>
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Role { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; } = true;
public User()
{
Id = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
}
}
// Repository
public interface IUserRepository : IMongoRepository<User, Guid>
{
Task<User> GetByEmailAsync(string email);
Task<IReadOnlyList<User>> GetActiveUsersAsync();
}
public class UserRepository : IUserRepository
{
private readonly IMongoRepository<User, Guid> _repository;
public UserRepository(IMongoRepository<User, Guid> repository)
{
_repository = repository;
}
public IMongoCollection<User> Collection => _repository.Collection;
// Implement all interface methods...
public Task<User> GetByEmailAsync(string email) =>
_repository.GetAsync(x => x.Email == email);
public Task<IReadOnlyList<User>> GetActiveUsersAsync() =>
_repository.FindAsync(x => x.IsActive);
}
// Seeder
public class DataSeeder : IMongoDbSeeder
{
private readonly IUserRepository _userRepository;
public DataSeeder(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task SeedAsync()
{
if (await _userRepository.ExistsAsync(x => true))
return;
await _userRepository.AddAsync(new User
{
Name = "Admin",
Email = "admin@example.com",
Role = "Admin"
});
}
}
Best Practices
- Use repository pattern - Encapsulate data access logic
- Implement proper indexing - Create indexes for frequently queried fields
- Handle transactions carefully - Use sessions for multi-document operations
- Use projections - Select only needed fields for better performance
- Implement proper error handling - Handle MongoDB-specific exceptions
- Use bulk operations - For multiple document operations
- Monitor performance - Use MongoDB profiler and monitoring tools
- Implement proper logging - Log database operations for debugging
Testing
Integration Testing
public class UserRepositoryTests : IClassFixture<MongoTestFixture>
{
private readonly IUserRepository _repository;
public UserRepositoryTests(MongoTestFixture fixture)
{
_repository = fixture.GetService<IUserRepository>();
}
[Fact]
public async Task AddAsync_ValidUser_StoresInDatabase()
{
// Arrange
var user = new User("Test User", "test@example.com");
// Act
await _repository.AddAsync(user);
// Assert
var stored = await _repository.GetAsync(user.Id);
Assert.NotNull(stored);
Assert.Equal(user.Name, stored.Name);
Assert.Equal(user.Email, stored.Email);
}
}
public class MongoTestFixture : IDisposable
{
private readonly ServiceProvider _serviceProvider;
public MongoTestFixture()
{
var services = new ServiceCollection();
services.AddConvey()
.AddMongo(x => x
.WithConnectionString("mongodb://localhost:27017")
.WithDatabase("test_db")
.WithRandomDatabaseSuffix(true)
);
services.AddScoped<IUserRepository, UserRepository>();
_serviceProvider = services.BuildServiceProvider();
}
public T GetService<T>() => _serviceProvider.GetRequiredService<T>();
public void Dispose() => _serviceProvider?.Dispose();
}
Troubleshooting
Common Issues
- Connection string errors
- Verify MongoDB server is running
- Check connection string format
- Ensure network connectivity
- Serialization issues
- Check BSON conventions registration
- Verify entity property types are supported
- Use proper MongoDB attributes if needed
- Index creation failures
- Check for unique constraint violations
- Verify sufficient database permissions
- Ensure proper index key specifications
- Performance issues
- Add appropriate indexes
- Use projections to limit returned data
- Consider using aggregation pipelines for complex queries