I have a custom type UniversityId
(value type):
namespace TestEntityFramework;
public sealed class UniversityId(string value)
{
public string Value { get; } = value;
public override string ToString()
{
return Value;
}
}
public static class UniversityIdExtensions
{
public static bool IsForeignUniversity(this UniversityId universityId)
{
return universityId.ToString().Contains('-');
}
}
This is my database context:
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
namespace TestEntityFramework;
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options)
{
public DbSet<Student> Students { get; set; }
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
configurationBuilder.Properties<UniversityId>().HaveConversion<UniversityIdConverter>();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Student>().HasKey(s => s.Id);
MethodInfo methodInfo = typeof(UniversityIdExtensions)
.GetMethod(name: nameof(UniversityIdExtensions.IsForeignUniversity), types: [typeof(UniversityId)])!;
modelBuilder
.HasDbFunction(methodInfo)
.HasTranslation(args =>
{
ISqlExpressionFactory sqlExpressionFactory = this.GetService<ISqlExpressionFactory>();
SqlConstantExpression likePattern = sqlExpressionFactory.Constant(value: "%-%");
return sqlExpressionFactory.Like(match: args[0], pattern: likePattern);
});
}
}
And this is the converter:
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace TestEntityFramework;
public sealed class UniversityIdConverter() : ValueConverter<UniversityId, string>(
convertToProviderExpression: id => id.Value, convertFromProviderExpression: id => new UniversityId(id));
I have used ValueConverter
a lot in my DDD applications, but it is the first time that I try to create a custom DbFunction
, that I’m trying to use like this:
public static IResult Handler(DatabaseContext context)
{
IQueryable<Response> query = context.Students
.Where(s => s.UniversityId.IsForeignUniversity())
.Select(s => new Response(s.Id, s.Name, s.Country, s.UniversityId.ToString()));
return Results.Ok(query);
}
I am constantly getting the following error, even though I certainly do have a conversion:
Unable to create a 'DbContext' of type 'TestEntityFramework.DatabaseContext'. The exception 'The parameter 'universityId' for the DbFunction 'TestEntityFramework.UniversityIdExtensions.IsForeignUniversity(TestEntityFramework.Uni versityId)' has an invalid type 'UniversityId'. Ensure the parameter type can be mapped by the current provider.' was thrown while attempting to create an instance. For the different patterns supported at design time, see https: //go.microsoft.com/fwlink/?linkid=851728
This seems simple enough (it’s the minimal reproducible example that I could come with) however no matter how I try to do the translation, it fails (I also tried like this):
.HasTranslation(args => new SqlFunctionExpression(
functionName: "LIKE",
arguments:
[
args[0],
new SqlConstantExpression(
constantExpression: Expression.Constant("%-%"),
typeMapping: new StringTypeMapping(storeType: "nvarchar(max)", dbType: null)
)
],
nullable: false,
argumentsPropagateNullability: [false, false],
type: typeof(bool),
typeMapping: RelationalTypeMapping.NullMapping));
But still the same result. Can someone point me in the right direction? Thank you so much.
EDIT: Code after @Charlieface comment:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Student>().HasKey(s => s.Id);
modelBuilder.Entity<Student>().Property(s => s.UniversityId).HasMaxLength(32);
MethodInfo methodInfo = typeof(UniversityIdExtensions)
.GetMethod(name: nameof(UniversityIdExtensions.IsForeignUniversity), types: [typeof(UniversityId)])!;
modelBuilder
.HasDbFunction(methodInfo: methodInfo,
builderAction: dbf =>
{
dbf.HasParameter(name: "universityId", buildAction: p => p.HasStoreType("varchar(32)"))
.HasTranslation(args =>
{
ISqlExpressionFactory sqlExpressionFactory = this.GetService<ISqlExpressionFactory>();
SqlConstantExpression likePattern = sqlExpressionFactory.Constant(value: "%-%");
return sqlExpressionFactory.Like(match: args[0], pattern: likePattern);
});
});
}
This works (i.e., the migration is created) but I get an ObjectDisposedException
when calling the endpoint who uses the custom function.
EDIT 2: Looks like that was because of the way I applied migrations. However I got this:
Invalid cast from 'System.String' to 'TestEntityFramework.UniversityId'.
So I’m not sure that it worked although I could do the migration.
EDIT 3: Combining both approaches I got this, which almost works but I need to change the equal sign for a LIKE:
modelBuilder
.HasDbFunction(methodInfo: methodInfo,
builderAction: dbf =>
{
dbf.HasParameter(name: "universityId", buildAction: p => p.HasStoreType("varchar(32)"))
.HasTranslation(args =>
{
var universityId = args[0];
var likeExpression = new SqlBinaryExpression(
ExpressionType.Equal,
universityId,
new SqlConstantExpression(
Expression.Constant("%-%"),
new StringTypeMapping("varchar(32)", dbType: null)
),
typeof(bool),
new BoolTypeMapping("bool")
);
return likeExpression;
});
});
And this is the SQL generated:
SELECT `s`.`Id`, `s`.`Name`, `s`.`Country`, `s`.`UniversityId`
FROM `Students` AS `s`
WHERE `s`.`UniversityId` = '%-%'
9