Skip to content

Commit

Permalink
Setup(Mongo): Integrate MongoDB into the project
Browse files Browse the repository at this point in the history
  • Loading branch information
ktutak1337 committed Mar 13, 2024
1 parent 5243946 commit 1292c84
Show file tree
Hide file tree
Showing 17 changed files with 377 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Server/StellarChat.Server.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using StellarChat.Shared.Infrastructure.Exceptions;
using StellarChat.Shared.Infrastructure.Contexts;
using StellarChat.Shared.Infrastructure.Observability.Logging;
using StellarChat.Shared.Infrastructure.DAL.Mongo;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -11,6 +12,7 @@
builder.Services.AddErrorHandling();
builder.Services.AddContext();
builder.Host.UseLogging();
builder.Services.AddMongo(builder.Configuration);

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace StellarChat.Shared.Abstractions.Pagination;

public interface IPagedQuery
{
int Page { get; set; }
int PageSize { get; set; }
}

public interface IPagedQuery<T> : IPagedQuery { }
36 changes: 36 additions & 0 deletions src/Shared/StellarChat.Shared.Abstractions/Pagination/Paged.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace StellarChat.Shared.Abstractions.Pagination;

public class Paged<T> : PagedBase
{
public IReadOnlyList<T> Items { get; set; } = Array.Empty<T>();
public bool Empty => Items is null || !Items.Any();
public bool HasNextPage => CurrentPage * ResultsPerPage < TotalResults;
public bool HasPreviousPage => CurrentPage > 1;

public Paged()
{
TotalPages = 1;
}

public Paged(IReadOnlyList<T> items,
int currentPage, int resultsPerPage,
int totalPages, long totalResults) :
base(currentPage, resultsPerPage, totalPages, totalResults)
{
Items = items;
}

public static Paged<T> Create(IReadOnlyList<T> items,
int currentPage, int resultsPerPage,
int totalPages, long totalResults)
=> new(items, currentPage, resultsPerPage, totalPages, totalResults);

public static Paged<T> From(PagedBase result, IReadOnlyList<T> items)
=> new(items, result.CurrentPage, result.ResultsPerPage,
result.TotalPages, result.TotalResults);

public static Paged<T> AsEmpty => new();

public Paged<TResult> Map<TResult>(Func<T, TResult> map)
=> Paged<TResult>.From(this, Items.Select(map).ToList());
}
20 changes: 20 additions & 0 deletions src/Shared/StellarChat.Shared.Abstractions/Pagination/PagedBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace StellarChat.Shared.Abstractions.Pagination;

public abstract class PagedBase
{
public int CurrentPage { get; set; }
public int ResultsPerPage { get; set; }
public int TotalPages { get; set; }
public long TotalResults { get; set; }

protected PagedBase() { }

protected PagedBase(int currentPage, int resultsPerPage,
int totalPages, long totalResults)
{
CurrentPage = currentPage;
ResultsPerPage = resultsPerPage;
TotalPages = totalPages;
TotalResults = totalResults;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace StellarChat.Shared.Abstractions.Pagination;

public abstract class PagedQuery : IPagedQuery
{
public int Page { get; set; } = 0;
public int PageSize { get; set; }
//public string OrderBy { get; set; } = string.Empty;
//public string SortOrder { get; set; } = string.Empty;
}

public abstract class PagedQuery<T> : PagedQuery, IPagedQuery<Paged<T>> { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.Extensions.Configuration;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Bson.Serialization;
using MongoDB.Bson;
using MongoDB.Driver;
using StellarChat.Shared.Infrastructure.DAL.Mongo.Factories;
using StellarChat.Shared.Infrastructure.DAL.Mongo.Seeders;
using Microsoft.Extensions.DependencyInjection;
using StellarChat.Shared.Infrastructure.DAL.Mongo.Repositories;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo;

public static class Extensions
{
public static IServiceCollection AddMongo(this IServiceCollection services, IConfiguration configuration, Type? seederType = null)
{
var section = configuration.GetSection("mongo");
var options = section.BindOptions<MongoOptions>();
services.Configure<MongoOptions>(section);

if (!section.Exists())
{
return services;
}

var mongoClient = new MongoClient(options.ConnectionString);
var database = mongoClient.GetDatabase(options.Database);
services.AddSingleton<IMongoClient>(mongoClient);
services.AddSingleton(database);

services.AddTransient<IMongoSessionFactory, MongoSessionFactory>();

if (seederType is null)
{
services.AddTransient<IMongoDbSeeder, MongoDbSeeder>();

using var scope = services.BuildServiceProvider().CreateScope();
var seeder = scope.ServiceProvider.GetRequiredService<IMongoDbSeeder>();
seeder.SeedAsync(database).GetAwaiter().GetResult();
}
else
{
services.AddTransient(typeof(IMongoDbSeeder), seederType);
}

RegisterConventions();

return services;
}

public static IServiceCollection AddMongoRepository<TEntity, TIdentifiable>(this IServiceCollection services, string collectionName)
where TEntity : IIdentifiable<TIdentifiable>
{
services.AddTransient<IMongoRepository<TEntity, TIdentifiable>>(sp =>
{
var database = sp.GetRequiredService<IMongoDatabase>();
return new MongoRepository<TEntity, TIdentifiable>(database, collectionName);
});

return services;
}

private static void RegisterConventions()
{
BsonSerializer.RegisterSerializer(typeof(decimal), new DecimalSerializer(BsonType.Decimal128));
BsonSerializer.RegisterSerializer(typeof(decimal?),
new NullableSerializer<decimal>(new DecimalSerializer(BsonType.Decimal128)));
ConventionRegistry.Register("mongo-conventions", new ConventionPack
{
new CamelCaseElementNameConvention(),
new IgnoreExtraElementsConvention(true),
new EnumRepresentationConvention(BsonType.String),
}, _ => true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using MongoDB.Driver;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo.Factories;

internal sealed class MongoSessionFactory : IMongoSessionFactory
{
private readonly IMongoClient _client;

public MongoSessionFactory(IMongoClient client)
=> _client = client;

public Task<IClientSessionHandle> CreateAsync()
=> _client.StartSessionAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using MongoDB.Driver;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo;

public interface IAppSettingsSeeder
{
Task SeedAsync(IMongoDatabase database);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace StellarChat.Shared.Infrastructure.DAL.Mongo;

public interface IIdentifiable<out T>
{
T Id { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using MongoDB.Driver;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo
{
public interface IMongoDbSeeder
{
Task SeedAsync(IMongoDatabase database);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using MongoDB.Driver;
using StellarChat.Shared.Abstractions.Pagination;
using System.Linq.Expressions;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo;

public interface IMongoRepository<TDocument, in TIdentifiable> where TDocument : IIdentifiable<TIdentifiable>
{
IMongoCollection<TDocument> Collection { get; }
IQueryable<TDocument> AsQueryable();
Task<TDocument> GetAsync(TIdentifiable id);
Task<TDocument> GetAsync(Expression<Func<TDocument, bool>> predicate);
Task<IReadOnlyList<TDocument>> FindAsync(Expression<Func<TDocument, bool>> predicate);
IEnumerable<TProjected> FilterBy<TProjected>(Expression<Func<TDocument, bool>> filterExpression, Expression<Func<TDocument, TProjected>> projectionExpression);
Task<Paged<TDocument>> BrowseAsync<TQuery>(Expression<Func<TDocument, bool>> predicate, TQuery query) where TQuery : IPagedQuery;
Task AddAsync(TDocument document);
Task AddManyAsync(ICollection<TDocument> documents);
Task UpdateAsync(TDocument document);
Task UpdateAsync(TDocument document, Expression<Func<TDocument, bool>> predicate);
Task DeleteAsync(TIdentifiable id);
Task DeleteAsync(Expression<Func<TDocument, bool>> predicate);
Task DeleteManyAsync(Expression<Func<TDocument, bool>> filterExpression);
Task<bool> ExistsAsync(Expression<Func<TDocument, bool>> predicate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using MongoDB.Driver;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo;

public interface IMongoSessionFactory
{
Task<IClientSessionHandle> CreateAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace StellarChat.Shared.Infrastructure.DAL.Mongo;

internal class MongoOptions
{
public string ConnectionString { get; set; } = null!;
public string Database { get; set; } = null!;
public bool DisableTransactions { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using StellarChat.Shared.Abstractions.Pagination;
using StellarChat.Shared.Infrastructure.DAL.Mongo;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo;

public static class Pagination
{
public static async Task<Paged<T>> PaginateAsync<T>(this IMongoQueryable<T> collection, IPagedQuery query)
=> await collection.PaginateAsync(query.Page, query.PageSize);

public static async Task<Paged<T>> PaginateAsync<T>(this IMongoQueryable<T> collection,
int page, int resultsPerPage)
{
if (page <= 0)
{
page = 1;
}
if (resultsPerPage <= 0)
{
resultsPerPage = 10;
}
var isEmpty = await collection.AnyAsync() == false;
if (isEmpty)
{
return Paged<T>.AsEmpty;
}
var totalResults = await collection.CountAsync();
var totalPages = (int)Math.Ceiling((decimal)totalResults / resultsPerPage);
var data = await collection.Limit(page, resultsPerPage).ToListAsync();

return Paged<T>.Create(data, page, resultsPerPage, totalPages, totalResults);
}

public static IMongoQueryable<T> Limit<T>(this IMongoQueryable<T> collection, IPagedQuery query)
=> collection.Limit(query.Page, query.PageSize);

public static IMongoQueryable<T> Limit<T>(this IMongoQueryable<T> collection,
int page, int resultsPerPage)
{
if (page <= 0)
{
page = 1;
}
if (resultsPerPage <= 0)
{
resultsPerPage = 10;
}
var skip = (page - 1) * resultsPerPage;
var data = collection.Skip(skip)
.Take(resultsPerPage);

return data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Linq.Expressions;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using StellarChat.Shared.Abstractions.Pagination;

namespace StellarChat.Shared.Infrastructure.DAL.Mongo.Repositories;

internal class MongoRepository<TDocument, TIdentifiable> : IMongoRepository<TDocument, TIdentifiable>
where TDocument : IIdentifiable<TIdentifiable>
{
public MongoRepository(IMongoDatabase database, string collectionName)
{
Collection = database.GetCollection<TDocument>(collectionName);
}

public IMongoCollection<TDocument> Collection { get; }

public IQueryable<TDocument> AsQueryable() => Collection.AsQueryable();

public Task<TDocument> GetAsync(TIdentifiable id)
=> GetAsync(e => e.Id.Equals(id));

Check warning on line 21 in src/Shared/StellarChat.Shared.Infrastructure/DAL/Mongo/Repositories/MongoRepository.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

public Task<TDocument> GetAsync(Expression<Func<TDocument, bool>> predicate)
=> Collection.Find(predicate).SingleOrDefaultAsync();

public async Task<IReadOnlyList<TDocument>> FindAsync(Expression<Func<TDocument, bool>> predicate)
=> await Collection.Find(predicate).ToListAsync();

public IEnumerable<TProjected> FilterBy<TProjected>(Expression<Func<TDocument, bool>> filterExpression, Expression<Func<TDocument, TProjected>> projectionExpression)
=> Collection.Find(filterExpression).Project(projectionExpression).ToEnumerable();

public Task<Paged<TDocument>> BrowseAsync<TQuery>(Expression<Func<TDocument, bool>> predicate,
TQuery query) where TQuery : IPagedQuery
=> Collection.AsQueryable().Where(predicate).PaginateAsync(query);

public Task AddAsync(TDocument document)
=> Collection.InsertOneAsync(document);

public async Task AddManyAsync(ICollection<TDocument> documents)
=> await Collection.InsertManyAsync(documents);

public Task UpdateAsync(TDocument document)
=> UpdateAsync(document, e => e.Id.Equals(document.Id));

public Task UpdateAsync(TDocument document, Expression<Func<TDocument, bool>> predicate)
=> Collection.ReplaceOneAsync(predicate, document);

public Task DeleteAsync(TIdentifiable id)
=> DeleteAsync(e => e.Id.Equals(id));

public Task DeleteAsync(Expression<Func<TDocument, bool>> predicate)
=> Collection.DeleteOneAsync(predicate);

public Task<bool> ExistsAsync(Expression<Func<TDocument, bool>> predicate)
=> Collection.Find(predicate).AnyAsync();

public async Task DeleteManyAsync(Expression<Func<TDocument, bool>> filterExpression)
=> await Collection.DeleteManyAsync(filterExpression);
}
Loading

0 comments on commit 1292c84

Please sign in to comment.