Skip to content

Commit

Permalink
Update ALl the new era!
Browse files Browse the repository at this point in the history
Why you need to define the QueryHandler?
Is this be a muse? I'm so happy.
The version should be a major!
  • Loading branch information
quyvu01 committed Dec 30, 2024
1 parent 58d97c0 commit 7f91d45
Show file tree
Hide file tree
Showing 16 changed files with 172 additions and 139 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Reflection;

namespace OfX.EntityFrameworkCore.ApplicationModels;

public sealed class OfXEfCoreRegistrar
{
public List<Type> DbContextTypes { get; private set; } = [];
public Assembly ModelConfigurationAssembly { get; private set; }

public void AddDbContexts(Type dbContext, params Type[] otherDbContextTypes) =>
DbContextTypes = [dbContext, ..otherDbContextTypes ?? []];

public void AddModelConfigurationsFromNamespaceContaining<TAssembly>() =>
ModelConfigurationAssembly = typeof(TAssembly).Assembly;
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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<TModel, TAttribute> : IQueryOfHandler<TModel, TAttribute>
public class EfQueryOfHandler<TModel, TAttribute>(
IServiceProvider serviceProvider,
string idPropertyName,
string defaultPropertyName)
: IQueryOfHandler<TModel, TAttribute>
where TModel : class
where TAttribute : OfXAttribute
{
private readonly string _idAlias;
private static readonly Lazy<ConcurrentDictionary<string, Expression<Func<TModel, OfXDataResponse>>>>
ExpressionMapModelStorage = new(() => []);

private const string DefaultIdAlias = "Id";

private readonly Func<RequestOf<TAttribute>, Expression<Func<TModel, bool>>> _filterFunction;
private readonly Expression<Func<TModel, OfXDataResponse>> _howToGetDefaultData;
private readonly DbSet<TModel> _collection;

/// <summary>
/// Note that the Id will automatically be selected. To modify this one, please update this method either
/// </summary>
protected virtual string SetIdAlias() => DefaultIdAlias;

private static readonly Lazy<ConcurrentDictionary<QueryExpressionData, Expression<Func<TModel, OfXDataResponse>>>>
LazyStorage = new(() => []);

protected EfQueryOfXHandler(IServiceProvider serviceProvider)
{
_filterFunction = SetFilter();
_howToGetDefaultData = SetHowToGetDefaultData();
ExceptionHelpers.ThrowIfNull(_filterFunction);
ExceptionHelpers.ThrowIfNull(_howToGetDefaultData);
_idAlias = SetIdAlias();
_collection = serviceProvider.GetRequiredService<GetEfDbContext>().Invoke(typeof(TModel))
.GetCollection<TModel>();
}

protected abstract Func<RequestOf<TAttribute>, Expression<Func<TModel, bool>>> SetFilter();

protected abstract Expression<Func<TModel, OfXDataResponse>> SetHowToGetDefaultData();

protected virtual Task HandleContextAsync(RequestContext<TAttribute> context) => Task.CompletedTask;
private readonly DbSet<TModel> _collection = serviceProvider.GetRequiredService<GetEfDbContext>()
.Invoke(typeof(TModel)).GetCollection<TModel>();

public async Task<ItemsResponse<OfXDataResponse>> GetDataAsync(RequestContext<TAttribute> 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<OfXDataResponse>(data);
}

private Expression<Func<TModel, OfXDataResponse>> BuildResponse(RequestOf<TAttribute> 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<Func<TModel, bool>> BuildFilter(RequestOf<TAttribute> 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<string>).GetMethod("Contains", [typeof(string)]);
var containsCall = Expression.Call(selectorsConstant, containsMethod!, property);
return Expression.Lambda<Func<TModel, bool>>(containsCall, parameter);
}

return LazyStorage.Value.GetOrAdd(new QueryExpressionData(request.Expression, typeof(TModel)), expressionData =>
private Expression<Func<TModel, OfXDataResponse>> BuildResponse(RequestOf<TAttribute> 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!);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
100 changes: 72 additions & 28 deletions src/OfX.EntityFrameworkCore/Extensions/EntityFrameworkExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<ConcurrentDictionary<Type, Type>> modelTypeLookUp = new(() => []);
private static readonly Type baseGenericType = typeof(EfQueryOfHandler<,>);
private static readonly Type interfaceGenericType = typeof(IQueryOfHandler<,>);

public static OfXServiceInjector AddOfXEFCore<TDbContext>(
this OfXServiceInjector ofXServiceInjector) where TDbContext : DbContext
public static OfXServiceInjector AddOfXEFCore(this OfXServiceInjector ofXServiceInjector,
Action<OfXEfCoreRegistrar> 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<IOfXEfDbContext>(sp =>
dbContextTypes.ForEach(dbContextType =>
{
var dbContext = sp.GetService<TDbContext>();
if (dbContext is null)
throw new OfXEntityFrameworkException.EntityFrameworkDbContextNotRegister(
"DbContext must be registered first!");
return new EfDbContextWrapped<TDbContext>(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<GetEfDbContext>(sp => modelType =>
{
if (modelTypeLookUp.Value.TryGetValue(modelType, out var serviceType))
Expand All @@ -41,29 +53,61 @@ public static OfXServiceInjector AddOfXEFCore<TDbContext>(
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);
});
}
}
2 changes: 1 addition & 1 deletion src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<LangVersion>default</LangVersion>
<Version>2.0.4</Version>
<Version>3.0.0</Version>
<Authors>Quy Vu</Authors>
<PackageId>OfX-EFCore</PackageId>
<Description>OfX extension. Use EntityFramework as Data Querying</Description>
Expand Down
51 changes: 15 additions & 36 deletions src/OfX.EntityFrameworkCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,51 +42,30 @@ builder.Services.AddOfXEntityFrameworkCore(cfg =>
cfg.AddAttributesContainNamespaces(typeof(WhereTheAttributeDefined).Assembly);
cfg.AddHandlersFromNamespaceContaining<SomeHandlerAssemblyMarker>();
})
.AddOfXEFCore<ServiceDbContext>();
```

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<User, UserOfAttribute>(serviceProvider)
.AddOfXEFCore(options =>
{
protected override Func<RequestOf<UserOfAttribute>, Expression<Func<User, bool>>> SetFilter() =>
q => u => q.SelectorIds.Contains(u.Id);

protected override Expression<Func<User, OfXDataResponse>> SetHowToGetDefaultData() =>
u => new OfXDataResponse { Id = u.Id, Value = u.Name };
}
options.AddDbContexts(typeof(TestDbContext));
options.AddModelConfigurationsFromNamespaceContaining<SomeModelAssemblyMarker>();
});
```

### 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<Func<TModel, bool>>`, 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<RequestOf<UserOfAttribute>, Expression<Func<User, bool>>> 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<Func<TModel, OfXDataResponse>>`, where `TModel` is your EF Core entity and `OfXDataResponse` is the mapped data structure.

Example:
```csharp
protected override Expression<Func<User, OfXDataResponse>> SetHowToGetDefaultData() =>
u => new OfXDataResponse { Id = u.Id, Value = u.Name };
[OfXConfigFor<UserOfAttribute>(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 |
|----------------------------------------------------------|-------------------------------------------------------------------------------------------------|--------------|---------------------------------------------------------------------------|
Expand Down
2 changes: 1 addition & 1 deletion src/OfX.Grpc/OfX.Grpc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<LangVersion>default</LangVersion>
<Version>2.0.3</Version>
<Version>3.0.0</Version>
<Authors>Quy Vu</Authors>
<PackageId>OfX-gRPC</PackageId>
<Description>OfX extension. Use gRPC as Data transporting</Description>
Expand Down
18 changes: 0 additions & 18 deletions src/OfX.Tests/EfHandlers/UserOfXHandler.cs

This file was deleted.

4 changes: 4 additions & 0 deletions src/OfX.Tests/Models/User.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using OfX.Attributes;
using OfX.Tests.Attributes;

namespace OfX.Tests.Models;

[OfXConfigFor<UserOfAttribute>(nameof(Id), nameof(Name))]
public class User
{
public string Id { get; set; }
Expand Down
Loading

0 comments on commit 7f91d45

Please sign in to comment.