Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public async Task<IPagedList<GiftVoucher>> Handle(GetGiftVoucherQuery request, C
query = query.Where(gc => gc.StoreId == request.StoreId || gc.StoreId == null || gc.StoreId == "");

if (!string.IsNullOrEmpty(request.PurchasedWithOrderItemId))
query = query.Where(gc => gc.PurchasedWithOrderItem.Id == request.PurchasedWithOrderItemId);
query = query.Where(gc =>
gc.PurchasedWithOrderItem != null && gc.PurchasedWithOrderItem.Id == request.PurchasedWithOrderItemId);

if (request.CreatedFromUtc.HasValue)
query = query.Where(gc => request.CreatedFromUtc.Value <= gc.CreatedOnUtc);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ public async Task<IPagedList<Order>> Handle(GetOrderQuery request, CancellationT

if (!string.IsNullOrEmpty(request.BillingLastName))
query = query.Where(o =>
o.BillingAddress != null && o.BillingAddress.LastName.Contains(request.BillingLastName));
o.BillingAddress != null && !string.IsNullOrEmpty(o.BillingAddress.LastName) &&
o.BillingAddress.LastName.Contains(request.BillingLastName));

if (!string.IsNullOrEmpty(request.OrderGuid))
if (Guid.TryParse(request.OrderGuid, out var orderGuid))
Expand Down
60 changes: 60 additions & 0 deletions src/Core/Grand.Data/LiteDb/LiteDBMapperWarmup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using LiteDB;
using System.Reflection;

namespace Grand.Data.LiteDb;

/// <summary>
/// Builds the entity mappers before the application starts handling requests and running tasks.
/// LiteDB publishes a mapper before filling its members and reads published mappers without a lock
/// (BsonMapper.BuildAddEntityMapper), so threads meeting a type for the first time together either
/// throw "Collection was modified" or silently read an object with fields missing.
/// </summary>
public static class LiteDBMapperWarmup
{
private static readonly string[] FrameworkNamespaces = ["System", "Microsoft", "LiteDB", "MongoDB"];

//Entity<T> is the public way to reach the internal BsonMapper.GetEntityMapper
private static readonly MethodInfo BuildMapper = typeof(BsonMapper).GetMethod(nameof(BsonMapper.Entity))!;

public static void Warm(IEnumerable<Type> entityTypes)
{
Warm(BsonMapper.Global, entityTypes);
}

public static void Warm(BsonMapper mapper, IEnumerable<Type> entityTypes)
{
var visited = new HashSet<Type>();
var pending = new Stack<Type>(entityTypes);

while (pending.TryPop(out var type))
{
if (!visited.Add(type)) continue;

if (type.IsArray)
{
pending.Push(type.GetElementType()!);
continue;
}

//lists, dictionaries and nullables hold their mapped types in the type arguments
foreach (var argument in type.GetGenericArguments()) pending.Push(argument);

if (type.IsEnum || type.ContainsGenericParameters || IsFrameworkType(type)) continue;

BuildMapper.MakeGenericMethod(type).Invoke(mapper, null);

foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
pending.Push(property.PropertyType);

//LINQ looks a member up on the type declaring it: x.Id goes through ParentEntity, not the entity
if (type.BaseType != null) pending.Push(type.BaseType);
foreach (var contract in type.GetInterfaces()) pending.Push(contract);
}
}

private static bool IsFrameworkType(Type type)
{
return type.Namespace != null &&
FrameworkNamespaces.Any(name => type.Namespace == name || type.Namespace.StartsWith(name + "."));
}
}
5 changes: 0 additions & 5 deletions src/Core/Grand.Infrastructure/Configuration/DatabaseConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ public class DatabaseConfig
/// </summary>
public bool UseLiteDb { get; set; }

/// <summary>
/// Gets or sets a value indicating whether use LiteDB database as a singleton service in the application
/// </summary>
public bool Singleton { get; set; }

/// <summary>
/// Gets or sets a value indicating whether use LiteDB database (only for installation process)
/// </summary>
Expand Down
19 changes: 13 additions & 6 deletions src/Core/Grand.Infrastructure/Startup/StartupApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
using Grand.Data.Audit;
using Grand.Data.LiteDb;
using Grand.Data.Mongo;
using Grand.Domain;
using Grand.Infrastructure.Audit;
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.Plugins;
using Grand.Infrastructure.TypeSearch;
using LiteDB;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
Expand Down Expand Up @@ -63,12 +66,9 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat
}
else
{
if (dbConfig.Singleton)
serviceCollection.AddSingleton(_ => new LiteDatabase(dataProviderSettings.ConnectionString)
{ UtcDate = true });
else
serviceCollection.AddScoped(_ => new LiteDatabase(dataProviderSettings.ConnectionString)
{ UtcDate = true });
//one shared instance: LiteDB locks the file, so a second one in the process cannot open it
serviceCollection.AddSingleton(_ => new LiteDatabase(dataProviderSettings.ConnectionString)
{ UtcDate = true });
}
}

Expand All @@ -87,6 +87,13 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat
}
else
{
//the mappers are not safe to build concurrently - startup is still single-threaded here
var typeSearcher = new TypeSearcher();
//an uninstalled plugin counts too: installing it uses its entities on the running site
var plugins = PluginManager.ReferencedPlugins?.Where(x => x.PluginType != null)
.Select(x => x.ReferencedAssembly) ?? [];
LiteDBMapperWarmup.Warm(
typeSearcher.ClassesOfType(typeof(BaseEntity), typeSearcher.GetAssemblies().Union(plugins)));
//database context
serviceCollection.AddScoped<IDatabaseContext, LiteDBContext>();
//store files context - grid fs
Expand Down
31 changes: 31 additions & 0 deletions src/Modules/Grand.Module.Api/Infrastructure/LiteDbMapperStartup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Grand.Data.LiteDb;
using Grand.Infrastructure;
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.TypeSearch;
using Grand.Module.Api.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Grand.Module.Api.Infrastructure;

public class LiteDbMapperStartup : IStartupApplication
{
public void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment)
{
}

public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
var dbConfig = new DatabaseConfig();
configuration.GetSection("Database").Bind(dbConfig);

//API queries read these models through TableCollection, from the mappers the entities are read from
if (dbConfig.UseLiteDb)
LiteDBMapperWarmup.Warm(new TypeSearcher().ClassesOfType<BaseApiEntityModel>());
}

public int Priority => 0;
public bool BeforeConfigure => false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Grand.Business.Checkout.Queries.Handlers.Orders;
using Grand.Business.Core.Queries.Checkout.Orders;
using Grand.Data.Tests.LiteDb;
using Grand.Domain.Orders;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Grand.Business.Checkout.Tests.Queries.Handlers.Orders;

[TestClass]
public class GetGiftVoucherQueryHandlerTests
{
private GetGiftVoucherQueryHandler _handler;
private LiteDBRepositoryMock<GiftVoucher> _repository;

[TestInitialize]
public void Init()
{
_repository = new LiteDBRepositoryMock<GiftVoucher>();
_handler = new GetGiftVoucherQueryHandler(_repository);
}

[TestMethod]
public async Task Handle_ByPurchasedOrderItem_SkipsVouchersCreatedByHand()
{
await _repository.InsertAsync(new GiftVoucher { Code = "manual" });
await _repository.InsertAsync(new GiftVoucher { Code = "bought", PurchasedWithOrderItem = new OrderItem { Id = "item-1" } });
await _repository.InsertAsync(new GiftVoucher { Code = "other", PurchasedWithOrderItem = new OrderItem { Id = "item-2" } });

var result = await _handler.Handle(new GetGiftVoucherQuery { PurchasedWithOrderItemId = "item-1" }, CancellationToken.None);

Assert.HasCount(1, result);
Assert.AreEqual("bought", result[0].Code);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Grand.Business.Checkout.Queries.Handlers.Orders;
using Grand.Business.Core.Queries.Checkout.Orders;
using Grand.Data.Tests.LiteDb;
using Grand.Data.Tests.MongoDb;
using Grand.Domain.Common;
using Grand.Domain.Orders;
using Grand.Domain.Payments;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -91,4 +93,31 @@ await _repository.Collection.InsertOneAsync(new Order {

Assert.AreEqual(0, result.Count);
}

[TestMethod]
public async Task Handle_ByBillingLastName_SkipsAddressesWithoutOne()
{
await _repository.Collection.InsertOneAsync(new Order { Code = "NONAME", BillingAddress = new Address() });
await _repository.Collection.InsertOneAsync(new Order { Code = "NAMED", BillingAddress = new Address { LastName = "Kowalski" } });

var result = await _handler.Handle(new GetOrderQuery { BillingLastName = "Kowal" }, CancellationToken.None);

Assert.AreEqual(1, result.Count);
Assert.AreEqual("NAMED", result[0].Code);
}

[TestMethod]
public async Task Handle_ByBillingLastName_OnLiteDb_SkipsAddressesWithoutOne()
{
//Table on LiteDB is LINQ over objects, so a null here throws instead of not matching
var repository = new LiteDBRepositoryMock<Order>();
await repository.InsertAsync(new Order { Code = "NONAME", BillingAddress = new Address() });
await repository.InsertAsync(new Order { Code = "NAMED", BillingAddress = new Address { LastName = "Kowalski" } });

var result = await new GetOrderQueryHandler(repository)
.Handle(new GetOrderQuery { BillingLastName = "Kowal" }, CancellationToken.None);

Assert.AreEqual(1, result.Count);
Assert.AreEqual("NAMED", result[0].Code);
}
}
Loading
Loading