diff --git a/src/OfX.EntityFrameworkCore/ApplicationModels/OfXEfCoreRegistrar.cs b/src/OfX.EntityFrameworkCore/ApplicationModels/OfXEfCoreRegistrar.cs new file mode 100644 index 0000000..34c33e0 --- /dev/null +++ b/src/OfX.EntityFrameworkCore/ApplicationModels/OfXEfCoreRegistrar.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace OfX.EntityFrameworkCore.ApplicationModels; + +public sealed class OfXEfCoreRegistrar +{ + public List DbContextTypes { get; private set; } = []; + public Assembly ModelConfigurationAssembly { get; private set; } + + public void AddDbContexts(Type dbContext, params Type[] otherDbContextTypes) => + DbContextTypes = [dbContext, ..otherDbContextTypes ?? []]; + + public void AddModelConfigurationsFromNamespaceContaining() => + ModelConfigurationAssembly = typeof(TAssembly).Assembly; +} \ No newline at end of file diff --git a/src/OfX.EntityFrameworkCore/ApplicationModels/OfXEfCoreServiceInjector.cs b/src/OfX.EntityFrameworkCore/ApplicationModels/OfXEfCoreServiceInjector.cs deleted file mode 100644 index 911e88f..0000000 --- a/src/OfX.EntityFrameworkCore/ApplicationModels/OfXEfCoreServiceInjector.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace OfX.EntityFrameworkCore.ApplicationModels; - -public sealed record OfXEfCoreServiceInjector(IServiceCollection Collection); \ No newline at end of file diff --git a/src/OfX.EntityFrameworkCore/ApplicationModels/QueryExpressionData.cs b/src/OfX.EntityFrameworkCore/ApplicationModels/QueryExpressionData.cs deleted file mode 100644 index b6cde9d..0000000 --- a/src/OfX.EntityFrameworkCore/ApplicationModels/QueryExpressionData.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace OfX.EntityFrameworkCore.ApplicationModels; - -public sealed record QueryExpressionData(string Expression, Type ModelType); diff --git a/src/OfX.EntityFrameworkCore/EfQueryOfXHandler.cs b/src/OfX.EntityFrameworkCore/EfQueryOfHandler.cs similarity index 67% rename from src/OfX.EntityFrameworkCore/EfQueryOfXHandler.cs rename to src/OfX.EntityFrameworkCore/EfQueryOfHandler.cs index 43f2636..e741184 100644 --- a/src/OfX.EntityFrameworkCore/EfQueryOfXHandler.cs +++ b/src/OfX.EntityFrameworkCore/EfQueryOfHandler.cs @@ -5,73 +5,57 @@ using Newtonsoft.Json; using OfX.Abstractions; using OfX.Attributes; -using OfX.EntityFrameworkCore.ApplicationModels; using OfX.EntityFrameworkCore.Delegates; -using OfX.Helpers; using OfX.Responses; namespace OfX.EntityFrameworkCore; -public abstract class EfQueryOfXHandler : IQueryOfHandler +public class EfQueryOfHandler( + IServiceProvider serviceProvider, + string idPropertyName, + string defaultPropertyName) + : IQueryOfHandler where TModel : class where TAttribute : OfXAttribute { - private readonly string _idAlias; + private static readonly Lazy>>> + ExpressionMapModelStorage = new(() => []); - private const string DefaultIdAlias = "Id"; - - private readonly Func, Expression>> _filterFunction; - private readonly Expression> _howToGetDefaultData; - private readonly DbSet _collection; - - /// - /// Note that the Id will automatically be selected. To modify this one, please update this method either - /// - protected virtual string SetIdAlias() => DefaultIdAlias; - - private static readonly Lazy>>> - LazyStorage = new(() => []); - - protected EfQueryOfXHandler(IServiceProvider serviceProvider) - { - _filterFunction = SetFilter(); - _howToGetDefaultData = SetHowToGetDefaultData(); - ExceptionHelpers.ThrowIfNull(_filterFunction); - ExceptionHelpers.ThrowIfNull(_howToGetDefaultData); - _idAlias = SetIdAlias(); - _collection = serviceProvider.GetRequiredService().Invoke(typeof(TModel)) - .GetCollection(); - } - - protected abstract Func, Expression>> SetFilter(); - - protected abstract Expression> SetHowToGetDefaultData(); - - protected virtual Task HandleContextAsync(RequestContext context) => Task.CompletedTask; + private readonly DbSet _collection = serviceProvider.GetRequiredService() + .Invoke(typeof(TModel)).GetCollection(); public async Task> GetDataAsync(RequestContext context) { - await HandleContextAsync(context); - var filter = _filterFunction.Invoke(context.Query); + var query = context.Query.Expression is null + ? context.Query with { Expression = defaultPropertyName } + : context.Query; var data = await _collection .AsNoTracking() - .Where(filter) - .Select(BuildResponse(context.Query)) + .Where(BuildFilter(context.Query)) + .Select(BuildResponse(query)) .ToListAsync(context.CancellationToken); return new ItemsResponse(data); } - private Expression> BuildResponse(RequestOf request) + // Todo: This is very first version, which is support for string. I need to define for specific type. This will be update later one! + private Expression> BuildFilter(RequestOf query) { - if (string.IsNullOrWhiteSpace(request.Expression)) return _howToGetDefaultData; + var parameter = Expression.Parameter(typeof(TModel), "x"); + var property = Expression.Property(parameter, idPropertyName); + var selectorsConstant = Expression.Constant(query.SelectorIds); + var containsMethod = typeof(List).GetMethod("Contains", [typeof(string)]); + var containsCall = Expression.Call(selectorsConstant, containsMethod!, property); + return Expression.Lambda>(containsCall, parameter); + } - return LazyStorage.Value.GetOrAdd(new QueryExpressionData(request.Expression, typeof(TModel)), expressionData => + private Expression> BuildResponse(RequestOf request) + { + return ExpressionMapModelStorage.Value.GetOrAdd(request.Expression, expression => { - var expression = expressionData.Expression; var parameter = Expression.Parameter(typeof(TModel), "x"); // Access the Id property on the model - var idProperty = Expression.Property(parameter, _idAlias); + var idProperty = Expression.Property(parameter, idPropertyName); var toStringMethod = typeof(object).GetMethod(nameof(ToString), Type.EmptyTypes); var idAsString = Expression.Call(idProperty, toStringMethod!); diff --git a/src/OfX.EntityFrameworkCore/Exceptions/OfXEntityFrameworkException.cs b/src/OfX.EntityFrameworkCore/Exceptions/OfXEntityFrameworkException.cs index 988384e..f2b70c4 100644 --- a/src/OfX.EntityFrameworkCore/Exceptions/OfXEntityFrameworkException.cs +++ b/src/OfX.EntityFrameworkCore/Exceptions/OfXEntityFrameworkException.cs @@ -6,4 +6,7 @@ public class EntityFrameworkDbContextNotRegister(string message) : Exception(mes public class ThereAreNoDbContextHasModel(Type modelType) : Exception($"There are no any db context contains model: {modelType.Name}"); + + public class DbContextsMustNotBeEmpty() + : Exception($"There are no any db contexts on AddOfXEFCore() method"); } \ No newline at end of file diff --git a/src/OfX.EntityFrameworkCore/Extensions/EntityFrameworkExtensions.cs b/src/OfX.EntityFrameworkCore/Extensions/EntityFrameworkExtensions.cs index e301a8a..1eefc7a 100644 --- a/src/OfX.EntityFrameworkCore/Extensions/EntityFrameworkExtensions.cs +++ b/src/OfX.EntityFrameworkCore/Extensions/EntityFrameworkExtensions.cs @@ -1,35 +1,47 @@ using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Emit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using OfX.Abstractions; +using OfX.Attributes; using OfX.EntityFrameworkCore.Abstractions; +using OfX.EntityFrameworkCore.ApplicationModels; using OfX.EntityFrameworkCore.Delegates; using OfX.EntityFrameworkCore.Exceptions; using OfX.EntityFrameworkCore.Services; -using OfX.Exceptions; using OfX.Extensions; using OfX.Registries; -using OfX.Statics; namespace OfX.EntityFrameworkCore.Extensions; public static class EntityFrameworkExtensions { private static readonly Lazy> modelTypeLookUp = new(() => []); + private static readonly Type baseGenericType = typeof(EfQueryOfHandler<,>); + private static readonly Type interfaceGenericType = typeof(IQueryOfHandler<,>); - public static OfXServiceInjector AddOfXEFCore( - this OfXServiceInjector ofXServiceInjector) where TDbContext : DbContext + public static OfXServiceInjector AddOfXEFCore(this OfXServiceInjector ofXServiceInjector, + Action registrarAction) { + var newOfXEfCoreRegistrar = new OfXEfCoreRegistrar(); + registrarAction.Invoke(newOfXEfCoreRegistrar); + var dbContextTypes = newOfXEfCoreRegistrar.DbContextTypes; + if (dbContextTypes.Count == 0) + throw new OfXEntityFrameworkException.DbContextsMustNotBeEmpty(); var serviceCollection = ofXServiceInjector.OfXRegister.ServiceCollection; - serviceCollection.AddScoped(sp => + dbContextTypes.ForEach(dbContextType => { - var dbContext = sp.GetService(); - if (dbContext is null) - throw new OfXEntityFrameworkException.EntityFrameworkDbContextNotRegister( - "DbContext must be registered first!"); - return new EfDbContextWrapped(dbContext); + serviceCollection.AddScoped(sp => + { + if (sp.GetService(dbContextType) is not DbContext dbContext) + throw new OfXEntityFrameworkException.EntityFrameworkDbContextNotRegister( + "DbContext must be registered first!"); + return (IOfXEfDbContext)Activator.CreateInstance( + typeof(EfDbContextWrapped<>).MakeGenericType(dbContextType), dbContext); + }); }); + serviceCollection.AddScoped(sp => modelType => { if (modelTypeLookUp.Value.TryGetValue(modelType, out var serviceType)) @@ -41,29 +53,61 @@ public static OfXServiceInjector AddOfXEFCore( modelTypeLookUp.Value.TryAdd(modelType, matchingServiceType.GetType()); return matchingServiceType; }); - AddEfQueryOfXHandlers(ofXServiceInjector); + AddEfQueryOfXHandlers(ofXServiceInjector, newOfXEfCoreRegistrar); return ofXServiceInjector; } - private static void AddEfQueryOfXHandlers(OfXServiceInjector serviceInjector) + private static void AddEfQueryOfXHandlers(OfXServiceInjector serviceInjector, OfXEfCoreRegistrar ofXEfCoreRegistrar) { - if (serviceInjector.OfXRegister.HandlersRegister is null) return; - var baseType = typeof(EfQueryOfXHandler<,>); - var interfaceType = typeof(IQueryOfHandler<,>); - serviceInjector.OfXRegister.HandlersRegister.ExportedTypes - .Where(t => + var modelsHasOfXConfig = ofXEfCoreRegistrar.ModelConfigurationAssembly + .ExportedTypes + .Where(a => a is { IsClass: true, IsAbstract: false, IsInterface: false }) + .Where(a => a.GetCustomAttributes().Any(x => { - var basedType = t.BaseType; - if (basedType is null || !basedType.IsGenericType) return false; - return t is { IsClass: true, IsAbstract: false } && basedType.GetGenericTypeDefinition() == baseType; - }) - .ForEach(handlerType => + var attributeType = x.GetType(); + return attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition() == typeof(OfXConfigForAttribute<>); + })).Select(a => { - var args = handlerType.BaseType!.GetGenericArguments(); - var parentType = interfaceType.MakeGenericType(args); - if (!OfXStatics.InternalQueryMapHandler.TryAdd(args[1], parentType)) - throw new OfXException.RequestMustNotBeAddMoreThanOneTimes(); - serviceInjector.OfXRegister.ServiceCollection.TryAddScoped(parentType, handlerType); + var attributes = a.GetCustomAttributes(); + var configAttribute = attributes.Select(x => + { + var attributeType = x.GetType(); + if (!attributeType.IsGenericType) return (null, null); + if (attributeType.GetGenericTypeDefinition() != typeof(OfXConfigForAttribute<>)) + return (null, null); + return (OfXConfigAttribute: x, OfXAttribute: attributeType.GetGenericArguments()[0]); + }).First(x => x is { OfXConfigAttribute: not null, OfXAttribute: not null }); + return (ModelType: a, OfXAttributeData: configAttribute.OfXConfigAttribute as IOfXConfigAttribute, + configAttribute.OfXAttribute); }); + + modelsHasOfXConfig.ForEach(m => + { + var assemblyName = new AssemblyName("EfQueryOfHandlerModule"); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule("EfQueryOfHandlerModule"); + + var baseType = baseGenericType.MakeGenericType(m.ModelType, m.OfXAttribute); + var typeBuilder = moduleBuilder.DefineType($"{m.ModelType.Name}EfQueryOfHandler", + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class, baseType); + + // Define the constructor + var baseCtor = baseType.GetConstructor([typeof(IServiceProvider), typeof(string), typeof(string)])!; + var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, + [typeof(IServiceProvider)]); + var ctorIL = ctorBuilder.GetILGenerator(); + ctorIL.Emit(OpCodes.Ldarg_0); // Load "this" + ctorIL.Emit(OpCodes.Ldarg_1); // Load IServiceProvider argument + ctorIL.Emit(OpCodes.Ldstr, m.OfXAttributeData.IdProperty); // Load "IdProperty" string argument + ctorIL.Emit(OpCodes.Ldstr, m.OfXAttributeData.DefaultProperty); // Load "DefaultProperty" string argument + ctorIL.Emit(OpCodes.Call, baseCtor); // Call the base constructor + ctorIL.Emit(OpCodes.Ret); + + // Create the dynamic type + var dynamicType = typeBuilder.CreateType(); + serviceInjector.OfXRegister.ServiceCollection.AddScoped( + interfaceGenericType.MakeGenericType(m.ModelType, m.OfXAttribute), dynamicType); + }); } } \ No newline at end of file diff --git a/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj b/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj index b968c47..57db4ab 100644 --- a/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj +++ b/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj @@ -4,7 +4,7 @@ enable net9.0;net8.0 default - 2.0.4 + 3.0.0 Quy Vu OfX-EFCore OfX extension. Use EntityFramework as Data Querying diff --git a/src/OfX.EntityFrameworkCore/README.md b/src/OfX.EntityFrameworkCore/README.md index 4760664..a9e8078 100644 --- a/src/OfX.EntityFrameworkCore/README.md +++ b/src/OfX.EntityFrameworkCore/README.md @@ -42,51 +42,30 @@ builder.Services.AddOfXEntityFrameworkCore(cfg => cfg.AddAttributesContainNamespaces(typeof(WhereTheAttributeDefined).Assembly); cfg.AddHandlersFromNamespaceContaining(); }) -.AddOfXEFCore(); -``` - -After installing the package OfX-EFCore, you can use the extension method `RegisterOfXEntityFramework()`, which takes two arguments: the `DbContext` and the handlers assembly. - -### 2. Write a Handler Using EF Core - -Implement a request handler to fetch the required data using Entity Framework Core. For example: - -```csharp -public sealed class UserOfXHandler(IServiceProvider serviceProvider) - : EfQueryOfXHandler(serviceProvider) +.AddOfXEFCore(options => { - protected override Func, Expression>> SetFilter() => - q => u => q.SelectorIds.Contains(u.Id); - - protected override Expression> SetHowToGetDefaultData() => - u => new OfXDataResponse { Id = u.Id, Value = u.Name }; -} + options.AddDbContexts(typeof(TestDbContext)); + options.AddModelConfigurationsFromNamespaceContaining(); +}); ``` -### Function Details - -#### `SetFilter` -This function is used to define the filter for querying data. It takes the query (`GetUserOfXQuery`) as an argument and returns an `Expression>`, where `TModel` is your EF Core entity. +After installing the package OfX-EFCore, you can use the method `AddDbContexts()`, which takes `DbContext(s)` to executing. +### 2. Mark the model you want to use with OfXAttribute Example: -```csharp -protected override Func, Expression>> SetFilter() => - q => u => q.SelectorIds.Contains(u.Id); -``` -Here, `SetFilter` ensures that only entities matching the provided `SelectorIds` in the query are retrieved. - -#### `SetHowToGetDefaultData` -This function specifies how to map the retrieved entity to the default data format. It returns an `Expression>`, where `TModel` is your EF Core entity and `OfXDataResponse` is the mapped data structure. -Example: ```csharp -protected override Expression> SetHowToGetDefaultData() => - u => new OfXDataResponse { Id = u.Id, Value = u.Name }; +[OfXConfigFor(nameof(Id), nameof(Name))] +public class User +{ + public string Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } +} ``` -Here, `SetHowToGetDefaultData` maps the `Id` and `Name` of the `User` entity to the `OfXDataResponse` format. - -By overriding these functions, you can customize the filtering logic and data mapping behavior to suit your application's requirements. +That all! Let go to the moon! +Note: In this release, Id is exclusively supported as a string. But hold tight—I'm gearing up to blow your mind with the next update! Stay tuned! | Package Name | Description | .NET Version | Document | |----------------------------------------------------------|-------------------------------------------------------------------------------------------------|--------------|---------------------------------------------------------------------------| diff --git a/src/OfX.Grpc/OfX.Grpc.csproj b/src/OfX.Grpc/OfX.Grpc.csproj index f77dd07..464ae32 100644 --- a/src/OfX.Grpc/OfX.Grpc.csproj +++ b/src/OfX.Grpc/OfX.Grpc.csproj @@ -4,7 +4,7 @@ enable net9.0;net8.0 default - 2.0.3 + 3.0.0 Quy Vu OfX-gRPC OfX extension. Use gRPC as Data transporting diff --git a/src/OfX.Tests/EfHandlers/UserOfXHandler.cs b/src/OfX.Tests/EfHandlers/UserOfXHandler.cs deleted file mode 100644 index 97af9c8..0000000 --- a/src/OfX.Tests/EfHandlers/UserOfXHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Linq.Expressions; -using OfX.Abstractions; -using OfX.EntityFrameworkCore; -using OfX.Responses; -using OfX.Tests.Attributes; -using OfX.Tests.Models; - -namespace OfX.Tests.EfHandlers; - -public class UserOfXHandler(IServiceProvider serviceProvider) - : EfQueryOfXHandler(serviceProvider) -{ - protected override Func, Expression>> SetFilter() => - q => c => q.SelectorIds.Contains(c.Id); - - protected override Expression> SetHowToGetDefaultData() => - u => new OfXDataResponse { Id = u.Id, Value = u.Name }; -} \ No newline at end of file diff --git a/src/OfX.Tests/Models/User.cs b/src/OfX.Tests/Models/User.cs index 989061d..b3a80f7 100644 --- a/src/OfX.Tests/Models/User.cs +++ b/src/OfX.Tests/Models/User.cs @@ -1,5 +1,9 @@ +using OfX.Attributes; +using OfX.Tests.Attributes; + namespace OfX.Tests.Models; +[OfXConfigFor(nameof(Id), nameof(Name))] public class User { public string Id { get; set; } diff --git a/src/OfX.Tests/OfXCoreTests.cs b/src/OfX.Tests/OfXCoreTests.cs index db4cf1a..1bd3908 100644 --- a/src/OfX.Tests/OfXCoreTests.cs +++ b/src/OfX.Tests/OfXCoreTests.cs @@ -30,7 +30,11 @@ public OfXCoreTests() c.AddGrpcHostWithOfXAttributes("localhost:5001", [typeof(UserOfAttribute)]); }); }) - .AddOfXEFCore(); + .AddOfXEFCore(options => + { + options.AddDbContexts(typeof(TestDbContext)); + options.AddModelConfigurationsFromNamespaceContaining(); + }); }) .InstallAllServices(); var dbContext = ServiceProvider.GetRequiredService(); diff --git a/src/OfX.Tests/OfXGrpcTests.cs b/src/OfX.Tests/OfXGrpcTests.cs index f3a185a..658fc7a 100644 --- a/src/OfX.Tests/OfXGrpcTests.cs +++ b/src/OfX.Tests/OfXGrpcTests.cs @@ -33,7 +33,11 @@ public void Should_Throw_Exception_When_Trying_To_Add_Same_Grpc_Host() c.AddGrpcHostWithOfXAttributes("localhost:5001", [typeof(ProvinceOfAttribute)]); }); }) - .AddOfXEFCore(); + .AddOfXEFCore(options => + { + options.AddDbContexts(typeof(TestDbContext)); + options.AddModelConfigurationsFromNamespaceContaining(); + }); }) .InstallAllServices(); } @@ -65,7 +69,11 @@ public void Should_Throw_Exception_When_Trying_To_Add_Existed_Attribute() [typeof(UserOfAttribute), typeof(ProvinceOfAttribute)]); }); }) - .AddOfXEFCore(); + .AddOfXEFCore(options => + { + options.AddDbContexts(typeof(TestDbContext)); + options.AddModelConfigurationsFromNamespaceContaining(); + }); }) .InstallAllServices(); } diff --git a/src/OfX/Abstractions/IOfXConfigAttribute.cs b/src/OfX/Abstractions/IOfXConfigAttribute.cs new file mode 100644 index 0000000..10df932 --- /dev/null +++ b/src/OfX/Abstractions/IOfXConfigAttribute.cs @@ -0,0 +1,7 @@ +namespace OfX.Abstractions; + +public interface IOfXConfigAttribute +{ + public string IdProperty { get; } + public string DefaultProperty { get; } +} \ No newline at end of file diff --git a/src/OfX/Attributes/OfXConfigForAttribute.cs b/src/OfX/Attributes/OfXConfigForAttribute.cs new file mode 100644 index 0000000..5e7625f --- /dev/null +++ b/src/OfX/Attributes/OfXConfigForAttribute.cs @@ -0,0 +1,11 @@ +using OfX.Abstractions; + +namespace OfX.Attributes; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class OfXConfigForAttribute(string idProperty, string defaultProperty) + : Attribute, IOfXConfigAttribute where TAttribute : OfXAttribute +{ + public string IdProperty { get; } = idProperty; + public string DefaultProperty { get; } = defaultProperty; +} \ No newline at end of file diff --git a/src/OfX/OfX.csproj b/src/OfX/OfX.csproj index e26866e..e43c2a8 100644 --- a/src/OfX/OfX.csproj +++ b/src/OfX/OfX.csproj @@ -4,7 +4,7 @@ enable net9.0;net8.0 default - 2.0.3 + 3.0.0 Quy Vu OfX The high performance and easiest way to play with microservices for .NET