Skip to content

Commit 5d17856

Browse files
authored
Create README.md
1 parent 4899c53 commit 5d17856

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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

Comments
 (0)