In EF8 now it is possible to include any row SQL query that is returning any C#/.NET type, without the need to include that type in the EF model. Executing arbitrary SQL statements or Stored Procedures is much easier than before.
1 New in EF8
In EF8 now it is possible to include any row SQL query that is returning any mappable C#/.NET type, without the need to include that type in the EF model. In the past, that type needed to be included in the EF model. The practical result is that executing arbitrary SQL statements or Stored Procedures is much easier than before.
Such queries can be executed using SqlQuery
[3] or SqlQueryRow
[4]. The result is of type System.Linq.IQueryable<TResult>
and in most cases can be subject to further LINQ manipulation. Here are the signatures of these two extension methods:
public static System.Linq.IQueryable<TResult> SqlQuery<TResult> (
this Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade databaseFacade,
FormattableString sql);
public static System.Linq.IQueryable<TResult> SqlQueryRaw<TResult> (
this Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade databaseFacade,
string sql, params object[] parameters);
Let us demo new features via a few examples.
2 Tutorial examples - Example1
2.1 Using Northwind database
For our examples, we will use the famous Northwind database for SqlServer. Just notice, that we will not create any EF model, it will be completely empty. That is to showcase the new approach, that even without EF model, we can still execute SQL queries and Stored Procedures and do some LINQ.
Here is what our EF model context looks like:
public partial class NorthwindContext : DbContext
{
public NorthwindContext(DbContextOptions<NorthwindContext> options)
: base(options)
{
}
}
namespace Example1
{
internal class NorthwindContextFactory : IDesignTimeDbContextFactory<NorthwindContext>
{
static NorthwindContextFactory()
{
IConfiguration config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true)
.Build();
connectionString = config["ConnectionStrings:NorthwindConnection"];
Console.WriteLine("ConnectionString:" + connectionString);
}
static string? connectionString = null;
public NorthwindContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<NorthwindContext>();
optionsBuilder.UseSqlServer(connectionString);
return new NorthwindContext(optionsBuilder.Options);
}
}
}
2.2 Row SQL query returning non-entity scalar
Since EF7 it is possible to write row SQL queries that are returning non-entity scalar (see[1]). Here are some examples.
List<string> top3Territories = ctx.Database.SqlQuery<string>($"SELECT TOP (3) [TerritoryID] FROM [Territories]").ToList();
int numberOfTeritories = ctx.Database.SqlQuery<int>($"SELECT COUNT(*) AS [Value] FROM [Territories]").SingleOrDefault();
List<string> top5TerritoriesOrdered = ctx.Database.SqlQuery<string>($"SELECT TOP (5) [TerritoryID] AS [Value] FROM [Territories]")
.OrderByDescending(p => p).ToList();
Please note that in examples 1.2 and 1.3 in order to apply LINQ to results, we needed to name the output column “Value”.
2.3 Row SQL query returning non-entity type
Since EF8 it is possible to write row SQL queries that are returning any mappable non-entity type. Here are some examples.
public class Territory
{
public string? TerritoryID { get; set; }
public string? TerritoryDescription { get; set; }
public int RegionID { get; set; }
};
List<Territory> top3TerritoriesType = ctx.Database.SqlQuery<Territory>($"SELECT TOP (3) * FROM [Territories]").ToList();
List<Territory> top3TerritoriesFiltered = ctx.Database.SqlQuery<Territory>($"SELECT * FROM [Territories]")
.Where(p=>p.RegionID==4).ToList();
Please note that in the above examples properties of the mapped type need to correspond to names of values in the result set, which in our case above are names of columns in the table. The type used does not need to match any table or view in the database in general.
Please note that in example 1.5 we are relying on the fact that SqlQuery
method returns IQueryable, which can be subject to further LINQ expressions.
2.4 Stored Procedure returning non-entity type
Executing stored procedures is just a special case of row SQL execution. Here are some examples.
public class SalesItem
{
public DateTime? ShippedDate { get; set; }
public int OrderID { get; set; }
public Decimal? Subtotal { get; set; }
public string? Year { get; set; }
};
List<SalesItem> salesByYear = ctx.Database.SqlQuery<SalesItem>(
$"exec [Sales by Year] @Beginning_Date = '1996-01-01' ,@Ending_Date ='1996-07-15'").ToList();
Documentation [2] suggests there might be issues trying to use LINQ on results returned from expressions like the above.
2.5 Interpolated Row SQL query returning non-entity type
The SqlQuery
method can use string interpolation to parametrize the query. At the first moment, it looks like it is not safe regarding the “SQL injection” problem, but in reality, it is safe since in the background real parametrization is done properly.
int RegionID = 4;
List<Territory> top3TerritoriesFiltered2 = ctx.Database.SqlQuery<Territory>(
$"SELECT * FROM [Territories] WHERE RegionID={RegionID}")
.ToList();
var start = new DateOnly(1996, 1, 1);
var end = new DateOnly(1996, 7, 15);
List<SalesItem> salesByYear2 = ctx.Database.SqlQuery<SalesItem>(
$"exec [Sales by Year] @Beginning_Date = {start} ,@Ending_Date ={end}").ToList();
2.6 Parametrized Row SQL query returning non-entity type
If you want to have complete control over the parametrization of your SQL query, you can use the method SqlQueryRow
.
var RegionIDParam = new SqlParameter("@RegionID", 4);
List<Territory> top3TerritoriesFiltered3 = ctx.Database.SqlQueryRaw<Territory>(
$"SELECT * FROM [Territories] WHERE RegionID=@RegionID", RegionIDParam)
.ToList();
var startParam = new SqlParameter("@start", new DateOnly(1996, 1, 1));
var endParam = new SqlParameter("@end", new DateOnly(1996, 7, 15));
var params1= new SqlParameter[] { startParam, endParam };
List<SalesItem> salesByYear3 = ctx.Database.SqlQueryRaw<SalesItem>(
$"exec [Sales by Year] @Beginning_Date = @start ,@Ending_Date =@end",
params1).ToList();
3 Some practical applications
Now, since we have the ability to run row SQL queries much easier than before, let us see some practical applications. We will show some row SQL queries to achieve the following:
- finding the number of tables in the database
- finding the number of views in the database
- finding the number of stored procedures in the database
- finding memory taken by database files
- finding full SqlServer version info
- finding short SqlServer version info
- finding SqlServer edition info
- finding current database name
Interestingly, the below code will work for any EF model/Database Context. Here are our examples:
FormattableString sql21 = $"SELECT COUNT(*) AS [Value] FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'";
int numberOfTablesInDatabase = ctx.Database.SqlQuery<int>(sql21).SingleOrDefault();
FormattableString sql22 = $"SELECT COUNT(*) AS [Value] FROM INFORMATION_SCHEMA.VIEWS ";
int numberOfViewsInDatabase = ctx.Database.SqlQuery<int>(sql22).SingleOrDefault();
FormattableString sql23 = $"SELECT COUNT(*) AS [Value] FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE'";
int numberOfStorProcsInDatabase = ctx.Database.SqlQuery<int>(sql23).SingleOrDefault();
public class DbFileInfo
{
public string? DbName { get; set; }
public string? FileName { get; set; }
public string? FileType { get; set; }
public Decimal CurrentSizeMB { get; set; }
public Decimal FreeSpaceMB { get; set; }
};
FormattableString sql24 =
@$"SELECT DB_NAME() AS DbName, name AS FileName, type_desc AS FileType,
size/128.0 AS CurrentSizeMB,
size/128.0 - CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT)/128.0 AS FreeSpaceMB
FROM sys.database_files WHERE type IN (0,1)";
List<DbFileInfo> dbMemoryInfo = ctx.Database.SqlQuery<DbFileInfo>(sql24).ToList();
FormattableString sql25 = $"Select @@version AS [Value]";
string? sqlServerVersionFull = ctx.Database.SqlQuery<string>(sql25).SingleOrDefault();
FormattableString sql26 = $"Select SERVERPROPERTY('productversion') AS [Value]";
string? sqlServerVersionShort = ctx.Database.SqlQuery<string>(sql26).SingleOrDefault();
FormattableString sql27 = $"Select SERVERPROPERTY('edition') AS [Value]";
string? sqlServerEdition = ctx.Database.SqlQuery<string>(sql27).SingleOrDefault();
FormattableString sql28 = $"Select DB_NAME() AS [Value]";
string? currentDatabaseName = ctx.Database.SqlQuery<string>(sql28).SingleOrDefault();
4 Full Code
Since most people like code they can copy-paste, here is the full code of the examples.
4.1 Example1
{
"ConnectionStrings": {
"NorthwindConnection": "Data Source=.;User Id=sa;Password=dbadmin1!;Initial Catalog=Northwind;Encrypt=False"
}
}
public partial class NorthwindContext : DbContext
{
public NorthwindContext(DbContextOptions<NorthwindContext> options)
: base(options)
{
}
}
namespace Example1
{
internal class NorthwindContextFactory : IDesignTimeDbContextFactory<NorthwindContext>
{
static NorthwindContextFactory()
{
IConfiguration config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true)
.Build();
connectionString = config["ConnectionStrings:NorthwindConnection"];
Console.WriteLine("ConnectionString:" + connectionString);
}
static string? connectionString = null;
public NorthwindContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<NorthwindContext>();
optionsBuilder.UseSqlServer(connectionString);
return new NorthwindContext(optionsBuilder.Options);
}
}
}
namespace Example1
{
internal class Program
{
public class Territory
{
public string? TerritoryID { get; set; }
public string? TerritoryDescription { get; set; }
public int RegionID { get; set; }
};
public class SalesItem
{
public DateTime? ShippedDate { get; set; }
public int OrderID { get; set; }
public Decimal? Subtotal { get; set; }
public string? Year { get; set; }
};
static void Main(string[] args)
{
try
{
Console.WriteLine("Hello, from Example1");
using NorthwindContext ctx = new NorthwindContextFactory().CreateDbContext(new string[0]);
Console.WriteLine("Example 1.1 ==================================");
List<string> top3Territories = ctx.Database.SqlQuery<string>($"SELECT TOP (3) [TerritoryID] FROM [Territories]").ToList();
string text11 = "top3Territories: ";
foreach (string id in top3Territories)
{
text11 += id + ", ";
}
Console.WriteLine(text11);
Console.WriteLine("Example 1.2 ==================================");
int numberOfTeritories = ctx.Database.SqlQuery<int>($"SELECT COUNT(*) AS [Value] FROM [Territories]").SingleOrDefault();
Console.WriteLine("numberOfTeritories: " + numberOfTeritories.ToString());
Console.WriteLine("Example 1.3 ==================================");
List<string> top5TerritoriesOrdered = ctx.Database.SqlQuery<string>($"SELECT TOP (5) [TerritoryID] AS [Value] FROM [Territories]")
.OrderByDescending(p => p).ToList();
string text13 = "top5TerritoriesOrdered: ";
foreach (string id in top5TerritoriesOrdered)
{
text13 += id + ", ";
}
Console.WriteLine(text13);
Console.WriteLine("Example 1.4 ==================================");
List<Territory> top3TerritoriesType = ctx.Database.SqlQuery<Territory>($"SELECT TOP (3) * FROM [Territories]").ToList();
string text14 = "top3Territories: ";
foreach (Territory ter in top3TerritoriesType)
{
text14 += $"\n ({ter.TerritoryID?.Trim()},{ter.TerritoryDescription?.Trim()},{ter.RegionID.ToString()})";
}
Console.WriteLine(text14);
Console.WriteLine("Example 1.5 ==================================");
List<Territory> top3TerritoriesFiltered = ctx.Database.SqlQuery<Territory>($"SELECT * FROM [Territories]")
.Where(p=>p.RegionID==4).ToList();
string text15 = "top3TerritoriesFiltered: ";
foreach (Territory ter in top3TerritoriesFiltered)
{
text15 += $"\n ({ter.TerritoryID?.Trim()},{ter.TerritoryDescription?.Trim()},{ter.RegionID.ToString()})";
}
Console.WriteLine(text15);
Console.WriteLine("Example 1.6 ==================================");
List<SalesItem> salesByYear = ctx.Database.SqlQuery<SalesItem>(
$"exec [Sales by Year] @Beginning_Date = '1996-01-01' ,@Ending_Date ='1996-07-15'").ToList();
string text16 = "salesByYear: ";
foreach (SalesItem item in salesByYear)
{
text16 += $"\n ({item.ShippedDate?.ToString()},{item.OrderID.ToString()}," +
$"{item.Subtotal?.ToString()},{item.Year?.Trim()})";
}
Console.WriteLine(text16);
Console.WriteLine("Example 1.7 ==================================");
int RegionID = 4;
List<Territory> top3TerritoriesFiltered2 = ctx.Database.SqlQuery<Territory>(
$"SELECT * FROM [Territories] WHERE RegionID={RegionID}")
.ToList();
string text17 = "top3TerritoriesFiltered2: ";
foreach (Territory ter in top3TerritoriesFiltered2)
{
text17 += $"\n ({ter.TerritoryID?.Trim()},{ter.TerritoryDescription?.Trim()},{ter.RegionID.ToString()})";
}
Console.WriteLine(text17);
Console.WriteLine("Example 1.8 ==================================");
var start = new DateOnly(1996, 1, 1);
var end = new DateOnly(1996, 7, 15);
List<SalesItem> salesByYear2 = ctx.Database.SqlQuery<SalesItem>(
$"exec [Sales by Year] @Beginning_Date = {start} ,@Ending_Date ={end}").ToList();
string text18 = "salesByYear2: ";
foreach (SalesItem item in salesByYear2)
{
text18 += $"\n ({item.ShippedDate?.ToString()},{item.OrderID.ToString()}," +
$"{item.Subtotal?.ToString()},{item.Year?.Trim()})";
}
Console.WriteLine(text18);
Console.WriteLine("Example 1.9 ==================================");
var RegionIDParam = new SqlParameter("@RegionID", 4);
List<Territory> top3TerritoriesFiltered3 = ctx.Database.SqlQueryRaw<Territory>(
$"SELECT * FROM [Territories] WHERE RegionID=@RegionID", RegionIDParam)
.ToList();
string text19 = "top3TerritoriesFiltered3: ";
foreach (Territory ter in top3TerritoriesFiltered3)
{
text19 += $"\n ({ter.TerritoryID?.Trim()},{ter.TerritoryDescription?.Trim()},{ter.RegionID.ToString()})";
}
Console.WriteLine(text19);
Console.WriteLine("Example 1.10 ==================================");
var startParam = new SqlParameter("@start", new DateOnly(1996, 1, 1));
var endParam = new SqlParameter("@end", new DateOnly(1996, 7, 15));
var params1= new SqlParameter[] { startParam, endParam };
List<SalesItem> salesByYear3 = ctx.Database.SqlQueryRaw<SalesItem>(
$"exec [Sales by Year] @Beginning_Date = @start ,@Ending_Date =@end",
params1).ToList();
string text110 = "salesByYear3: ";
foreach (SalesItem item in salesByYear3)
{
text110 += $"\n ({item.ShippedDate?.ToString()},{item.OrderID.ToString()}," +
$"{item.Subtotal?.ToString()},{item.Year?.Trim()})";
}
Console.WriteLine(text110);
}
catch (Exception ex)
{
Console.WriteLine("Exception: "+ex.ToString());
}
}
}
}
4.2 Example2
{
"ConnectionStrings": {
"NorthwindConnection": "Data Source=.;User Id=sa;Password=dbadmin1!;Initial Catalog=Northwind;Encrypt=False"
}
}
public partial class AnyDBContext : DbContext
{
public AnyDBContext(DbContextOptions<AnyDBContext> options)
: base(options)
{
}
}
namespace Example2
{
internal class AnyDBContextFactory : IDesignTimeDbContextFactory<AnyDBContext>
{
static AnyDBContextFactory()
{
IConfiguration config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true)
.Build();
connectionString = config["ConnectionStrings:NorthwindConnection"];
Console.WriteLine("ConnectionString:" + connectionString);
}
static string? connectionString = null;
public AnyDBContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<AnyDBContext>();
optionsBuilder.UseSqlServer(connectionString);
return new AnyDBContext(optionsBuilder.Options);
}
}
}
namespace Example2
{
internal class Program
{
public class DbFileInfo
{
public string? DbName { get; set; }
public string? FileName { get; set; }
public string? FileType { get; set; }
public Decimal CurrentSizeMB { get; set; }
public Decimal FreeSpaceMB { get; set; }
};
static void Main(string[] args)
{
try
{
Console.WriteLine("Hello, from Example2");
using AnyDBContext ctx = new AnyDBContextFactory().CreateDbContext(new string[0]);
Console.WriteLine("Example 2.1 ==================================");
FormattableString sql21 = $"SELECT COUNT(*) AS [Value] FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'";
int numberOfTablesInDatabase = ctx.Database.SqlQuery<int>(sql21).SingleOrDefault();
Console.WriteLine("numberOfTablesInDatabase: " + numberOfTablesInDatabase.ToString());
Console.WriteLine("Example 2.2 ==================================");
FormattableString sql22 = $"SELECT COUNT(*) AS [Value] FROM INFORMATION_SCHEMA.VIEWS ";
int numberOfViewsInDatabase = ctx.Database.SqlQuery<int>(sql22).SingleOrDefault();
Console.WriteLine("numberOfViewsInDatabase: " + numberOfViewsInDatabase.ToString());
Console.WriteLine("Example 2.3 ==================================");
FormattableString sql23 = $"SELECT COUNT(*) AS [Value] FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE'";
int numberOfStorProcsInDatabase = ctx.Database.SqlQuery<int>(sql23).SingleOrDefault();
Console.WriteLine("numberOfStorProcsInDatabase: " + numberOfStorProcsInDatabase.ToString());
Console.WriteLine("Example 2.4 ==================================");
FormattableString sql24 =
@$"SELECT DB_NAME() AS DbName, name AS FileName, type_desc AS FileType,
size/128.0 AS CurrentSizeMB,
size/128.0 - CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT)/128.0 AS FreeSpaceMB
FROM sys.database_files WHERE type IN (0,1)";
List<DbFileInfo> dbMemoryInfo = ctx.Database.SqlQuery<DbFileInfo>(sql24).ToList();
string text23 = "dbMemoryInfo: ";
foreach (DbFileInfo item in dbMemoryInfo)
{
text23 += $"\n (DbName:{item.DbName?.Trim()},FileName:{item.FileName?.Trim()},FileType:{item.FileType?.Trim()}," +
$"CurrentSizeMB:{item.CurrentSizeMB.ToString()},FreeSpaceMB:{item.FreeSpaceMB.ToString()})";
}
Console.WriteLine(text23);
Console.WriteLine("Example 2.5 ==================================");
FormattableString sql25 = $"Select @@version AS [Value]";
string? sqlServerVersionFull = ctx.Database.SqlQuery<string>(sql25).SingleOrDefault();
Console.WriteLine("sqlServerVersionFull: " + sqlServerVersionFull?.ToString());
Console.WriteLine("Example 2.6 ==================================");
FormattableString sql26 = $"Select SERVERPROPERTY('productversion') AS [Value]";
string? sqlServerVersionShort = ctx.Database.SqlQuery<string>(sql26).SingleOrDefault();
Console.WriteLine("sqlServerVersionShort: " + sqlServerVersionShort?.ToString());
Console.WriteLine("Example 2.7 ==================================");
FormattableString sql27 = $"Select SERVERPROPERTY('edition') AS [Value]";
string? sqlServerEdition = ctx.Database.SqlQuery<string>(sql27).SingleOrDefault();
Console.WriteLine("sqlServerEdition: " + sqlServerEdition?.ToString());
Console.WriteLine("Example 2.8 ==================================");
FormattableString sql28 = $"Select DB_NAME() AS [Value]";
string? currentDatabaseName = ctx.Database.SqlQuery<string>(sql28).SingleOrDefault();
Console.WriteLine("currentDatabaseName: " + currentDatabaseName?.ToString());
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.ToString());
}
}
}
}
5 References
[1] https://learn.microsoft.com/en-us/ef/core/querying/sql-queries#querying-scalar-(non-entity)-types
SQL Queries, Querying scalar (non-entity) types
[2] https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#raw-sql-queries-for-unmapped-types
What's New in EF Core 8, Raw SQL queries for unmapped types
[3] https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.sqlquery?view=efcore-8.0
RelationalDatabaseFacadeExtensions.SqlQuery<TResult> Method
[4] https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.sqlqueryraw?view=efcore-8.0
RelationalDatabaseFacadeExtensions.SqlQueryRaw<TResult> Method