Skip to content

Commit

Permalink
Merge branch 'refs/heads/dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
quyvu01 committed Dec 30, 2024
2 parents ec4df92 + 7f91d45 commit d378501
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 d378501

Please sign in to comment.