|
| 1 | +# EF Core 3.x support for SQL Server's `TRY_PARSE` function |
| 2 | +## Use |
| 3 | +Install the NuGet package: |
| 4 | +```powershell |
| 5 | +Install-Package NBS.EntityFrameworkCore.SqlServer.TryParse |
| 6 | +``` |
| 7 | + |
| 8 | +Register the functions in your `DbContext`'s `OnModelCreating` method: |
| 9 | +```csharp |
| 10 | +protected override void OnModelCreating(ModelBuilder modelBuilder) |
| 11 | +{ |
| 12 | + base.OnModelCreating(modelBuilder); |
| 13 | + TryParse.Register(modelBuilder); |
| 14 | +} |
| 15 | +``` |
| 16 | + |
| 17 | +Then call the functions as part of a query: |
| 18 | +```csharp |
| 19 | +var result = context.Set<SomeEntity>().Select(e => new { e.Id, e.Value, ValueInt32 = TryParse.Int32(e.Value) }).ToList(); |
| 20 | +``` |
| 21 | + |
| 22 | +This will generate the expected SQL: |
| 23 | +```sql |
| 24 | +SELECT Id, Value, TRY_PARSE(Value As int) As ValueInt32 FROM SomeEntities |
| 25 | +``` |
| 26 | + |
| 27 | +## Background |
| 28 | +[TRY_PARSE](https://docs.microsoft.com/en-us/sql/t-sql/functions/try-parse-transact-sql) was added in SQL Server 2012. However, EF Core 3.x does not support calling this function by default. |
| 29 | + |
| 30 | +Whilst EF Core provides methods to [map user-defined functions](https://docs.microsoft.com/en-us/ef/core/querying/database-functions), mapping `TRY_PARSE` is complicated by the way the arguments are passed. EF Core has great support for traditional functions, where the arguments are passed as a comma-separated list - eg: |
| 31 | +```sql |
| 32 | +dbo.SomeFunction(Foo.Bar, @b, 42) |
| 33 | +``` |
| 34 | + |
| 35 | +But for `TRY_PARSE`, the arguments are separated by spaces, not commas: |
| 36 | +```sql |
| 37 | +TRY_PARSE(Foo.Bar AS int) |
| 38 | +``` |
| 39 | + |
| 40 | +To enable this, it was necessary to implement a custom `SqlExpression` class to represent the parameter. This class needs to override both the `Print` and `Accept` methods in order to generate the correct SQL. |
| 41 | + |
| 42 | +```csharp |
| 43 | +internal sealed class TryParseArgumentExpression : SqlExpression |
| 44 | +{ |
| 45 | + private readonly SqlExpression _sourceExpression; |
| 46 | + private readonly SqlFragmentExpression _asExpression; |
| 47 | + |
| 48 | + public TryParseArgumentExpression(Type type, SqlExpression sourceExpression, string sqlTypeName) |
| 49 | + : base(type, sourceExpression.TypeMapping) |
| 50 | + { |
| 51 | + _sourceExpression = sourceExpression ?? throw new ArgumentNullException(nameof(sourceExpression)); |
| 52 | + _asExpression = new SqlFragmentExpression($" AS {sqlTypeName}"); |
| 53 | + } |
| 54 | + |
| 55 | + private TryParseArgumentExpression(Type type, SqlExpression sourceExpression, SqlFragmentExpression asExpression) |
| 56 | + : base(type, sourceExpression.TypeMapping) |
| 57 | + { |
| 58 | + _sourceExpression = sourceExpression ?? throw new ArgumentNullException(nameof(sourceExpression)); |
| 59 | + _asExpression = asExpression ?? throw new ArgumentNullException(nameof(asExpression)); |
| 60 | + } |
| 61 | + |
| 62 | + protected override Expression VisitChildren(ExpressionVisitor visitor) |
| 63 | + { |
| 64 | + var newSource = (SqlExpression?)visitor.Visit(_sourceExpression) ?? _sourceExpression; |
| 65 | + var newAsExpression = (SqlFragmentExpression?)visitor.Visit(_asExpression) ?? _asExpression; |
| 66 | + if (Equals(newSource, _sourceExpression) && Equals(newAsExpression, _asExpression)) return this; |
| 67 | + return new TryParseArgumentExpression(Type, newSource, newAsExpression); |
| 68 | + } |
| 69 | + |
| 70 | + protected override Expression Accept(ExpressionVisitor visitor) |
| 71 | + { |
| 72 | + visitor.Visit(_sourceExpression); |
| 73 | + visitor.Visit(_asExpression); |
| 74 | + return this; |
| 75 | + } |
| 76 | + |
| 77 | + public override void Print(ExpressionPrinter expressionPrinter) |
| 78 | + { |
| 79 | + expressionPrinter.Visit(_sourceExpression); |
| 80 | + expressionPrinter.Visit(_asExpression); |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +It was then possible to use this custom expression, along with an internal attribute which specifies the mapped SQL type name, to register the custom functions: |
| 86 | +```csharp |
| 87 | +public static void Register(ModelBuilder modelBuilder) |
| 88 | +{ |
| 89 | + foreach (var dbFunc in typeof(TryParse).GetMethods(BindingFlags.Public | BindingFlags.Static)) |
| 90 | + { |
| 91 | + var attribute = dbFunc.GetCustomAttribute<SqlTypeNameAttribute>(); |
| 92 | + if (attribute is null) continue; |
| 93 | + |
| 94 | + modelBuilder.HasDbFunction(dbFunc).HasTranslation(args => |
| 95 | + { |
| 96 | + var newArgs = args.ToList(); |
| 97 | + newArgs[0] = new TryParseArgumentExpression(dbFunc.ReturnType, newArgs[0], attribute.SqlTypeName); |
| 98 | + return SqlFunctionExpression.Create("TRY_PARSE", newArgs, dbFunc.ReturnType, null); |
| 99 | + }); |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
0 commit comments