[英]Raw SQL Query without DbSet - Entity Framework Core
使用 Entity Framework Core 刪除dbData.Database.SqlQuery<SomeModel>
我找不到為我的全文搜索查詢構建原始 SQL 查詢的解決方案,該查詢將返回表數據和排名。
我見過在 Entity Framework Core 中構建原始 SQL 查詢的唯一方法是通過dbData.Product.FromSql("SQL SCRIPT");
這沒有用,因為我沒有 DbSet,它將 map 我在查詢中返回的排名。
有任何想法嗎???
這取決於您使用的是EF Core 2.1還是EF Core 3 及更高版本。
如果您使用自 2018 年 5 月 7 日起可用的 EF Core 2.1 Release Candidate 1,您可以利用提議的新功能,即查詢類型。
什么是 查詢類型?
除了實體類型之外,EF Core 模型還可以包含查詢類型,可用於針對未映射到實體類型的數據執行數據庫查詢。
什么時候使用查詢類型?
作為即席 FromSql() 查詢的返回類型。
映射到數據庫視圖。
映射到沒有定義主鍵的表。
映射到模型中定義的查詢。
因此,您不再需要執行所有建議的技巧或解決方法來回答您的問題。 只需按照以下步驟操作:
首先,您定義了一個DbQuery<T>
類型的新屬性,其中T
是將攜帶 SQL 查詢的列值的類的類型。 因此,在您的DbContext
中,您將擁有以下內容:
public DbQuery<SomeModel> SomeModels { get; set; }
其次,像使用DbSet<T>
一樣使用FromSql
方法:
var result = context.SomeModels.FromSql("SQL_SCRIPT").ToList();
var result = await context.SomeModels.FromSql("SQL_SCRIPT").ToListAsync();
另請注意, DdContext
是部分類,因此您可以創建一個或多個單獨的文件來組織最適合您的“原始 SQL DbQuery”定義。
查詢類型現在稱為Keyless entity type 。 如上所述,查詢類型是在 EF Core 2.1 中引入的。 如果您使用的是 EF Core 3.0 或更高版本,您現在應該考慮使用無鍵實體類型,因為查詢類型現在被標記為過時。
此功能是在 EF Core 2.1 中以查詢類型的名稱添加的。 在 EF Core 3.0 中,該概念被重命名為無鍵實體類型。 [Keyless] 數據注釋在 EFCore 5.0 中可用。
對於何時使用無鍵實體類型,我們仍然有與查詢類型相同的場景。
因此,要使用它,您需要首先使用[Keyless]
數據注釋或使用.HasNoKey()
方法調用的流暢配置來標記您的類SomeModel
,如下所示:
public DbSet<SomeModel> SomeModels { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SomeModel>().HasNoKey();
}
完成該配置后,您可以使用此處介紹的方法之一來執行 SQL 查詢。 例如,您可以使用這個:
var result = context.SomeModels.FromSqlRaw("SQL SCRIPT").ToList();
var result = await context.SomeModels.FromSql("SQL_SCRIPT").ToListAsync();
在其他答案的基礎上,我編寫了這個完成任務的助手,包括示例用法:
public static class Helper
{
public static List<T> RawSqlQuery<T>(string query, Func<DbDataReader, T> map)
{
using (var context = new DbContext())
{
using (var command = context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = query;
command.CommandType = CommandType.Text;
context.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
var entities = new List<T>();
while (result.Read())
{
entities.Add(map(result));
}
return entities;
}
}
}
}
用法:
public class TopUser
{
public string Name { get; set; }
public int Count { get; set; }
}
var result = Helper.RawSqlQuery(
"SELECT TOP 10 Name, COUNT(*) FROM Users U"
+ " INNER JOIN Signups S ON U.UserId = S.UserId"
+ " GROUP BY U.Name ORDER BY COUNT(*) DESC",
x => new TopUser { Name = (string)x[0], Count = (int)x[1] });
result.ForEach(x => Console.WriteLine($"{x.Name,-25}{x.Count}"));
我計划在添加內置支持后立即擺脫它。 根據 EF Core 團隊的 Arthur Vickers 的一份聲明,這是 2.0 后的高優先級。 該問題正在此處進行跟蹤。
在 EF Core 中,您不再可以執行“免費”原始 sql。 您需要為該類定義一個 POCO 類和一個DbSet
。 在您的情況下,您將需要定義Rank :
var ranks = DbContext.Ranks
.FromSql("SQL_SCRIPT OR STORED_PROCEDURE @p0,@p1,...etc", parameters)
.AsNoTracking().ToList();
因為它肯定是只讀的,所以包含.AsNoTracking()
調用會很有用。
編輯 - EF Core 3.0 中的重大變化:
DbQuery()現在已過時,應(再次)使用DbSet( )。 如果你有一個無鍵實體,即它不需要主鍵,你可以使用HasNoKey()方法:
ModelBuilder.Entity<SomeModel>().HasNoKey()
更多信息可以在這里找到
現在,在 EFCore 有新內容之前,我會使用命令並手動映射它
using (var command = this.DbContext.Database.GetDbConnection().CreateCommand())
{
command.CommandText = "SELECT ... WHERE ...> @p1)";
command.CommandType = CommandType.Text;
var parameter = new SqlParameter("@p1",...);
command.Parameters.Add(parameter);
this.DbContext.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
while (result.Read())
{
.... // Map to your entity
}
}
}
盡量使用 SqlParameter 來避免 Sql Injection。
dbData.Product.FromSql("SQL SCRIPT");
FromSql 不適用於完整查詢。 例如,如果您想包含 WHERE 子句,它將被忽略。
一些鏈接:
您可以在 EF Core 中執行原始 sql - 將此類添加到您的項目中。 這將允許您執行原始 SQL 並獲得原始結果,而無需定義 POCO 和 DBSet。 有關原始示例,請參閱https://github.com/aspnet/EntityFramework/issues/1862#issuecomment-220787464 。
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.EntityFrameworkCore
{
public static class RDFacadeExtensions
{
public static RelationalDataReader ExecuteSqlQuery(this DatabaseFacade databaseFacade, string sql, params object[] parameters)
{
var concurrencyDetector = databaseFacade.GetService<IConcurrencyDetector>();
using (concurrencyDetector.EnterCriticalSection())
{
var rawSqlCommand = databaseFacade
.GetService<IRawSqlCommandBuilder>()
.Build(sql, parameters);
return rawSqlCommand
.RelationalCommand
.ExecuteReader(
databaseFacade.GetService<IRelationalConnection>(),
parameterValues: rawSqlCommand.ParameterValues);
}
}
public static async Task<RelationalDataReader> ExecuteSqlQueryAsync(this DatabaseFacade databaseFacade,
string sql,
CancellationToken cancellationToken = default(CancellationToken),
params object[] parameters)
{
var concurrencyDetector = databaseFacade.GetService<IConcurrencyDetector>();
using (concurrencyDetector.EnterCriticalSection())
{
var rawSqlCommand = databaseFacade
.GetService<IRawSqlCommandBuilder>()
.Build(sql, parameters);
return await rawSqlCommand
.RelationalCommand
.ExecuteReaderAsync(
databaseFacade.GetService<IRelationalConnection>(),
parameterValues: rawSqlCommand.ParameterValues,
cancellationToken: cancellationToken);
}
}
}
}
以下是如何使用它的示例:
// Execute a query.
using(var dr = await db.Database.ExecuteSqlQueryAsync("SELECT ID, Credits, LoginDate FROM SamplePlayer WHERE " +
"Name IN ('Electro', 'Nitro')"))
{
// Output rows.
var reader = dr.DbDataReader;
while (reader.Read())
{
Console.Write("{0}\t{1}\t{2} \n", reader[0], reader[1], reader[2]);
}
}
您可以使用它(來自https://github.com/aspnet/EntityFrameworkCore/issues/1862#issuecomment-451671168 ):
public static class SqlQueryExtensions
{
public static IList<T> SqlQuery<T>(this DbContext db, string sql, params object[] parameters) where T : class
{
using (var db2 = new ContextForQueryType<T>(db.Database.GetDbConnection()))
{
// share the current database transaction, if one exists
var transaction = db.CurrentTransaction;
if (transaction != null)
db2.Database.UseTransaction(transaction.GetDbTransaction());
return db2.Query<T>().FromSql(sql, parameters).ToList();
}
}
public static IList<T> SqlQuery<T>(this DbContext db, Func<T> anonType, string sql, params object[] parameters) where T : class
=> SqlQuery<T>(db, sql, parameters);
private class ContextForQueryType<T> : DbContext where T : class
{
private readonly DbConnection connection;
public ContextForQueryType(DbConnection connection)
{
this.connection = connection;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// switch on the connection type name to enable support multiple providers
// var name = con.GetType().Name;
optionsBuilder.UseSqlServer(connection, options => options.EnableRetryOnFailure());
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<T>().HasNoKey();
base.OnModelCreating(modelBuilder);
}
}
}
以及用法:
using (var db = new Db())
{
var results = db.SqlQuery<ArbitraryType>("select 1 id, 'joe' name");
//or with an anonymous type like this
var results2 = db.SqlQuery(() => new { id =1, name=""},"select 1 id, 'joe' name");
}
添加 Nuget 包 - Microsoft.EntityFrameworkCore.Relational
using Microsoft.EntityFrameworkCore;
...
await YourContext.Database.ExecuteSqlCommandAsync("... @p0, @p1", param1, param2 ..)
這會將行號作為 int 返回
試試這個:(創建擴展方法)
public static List<T> ExecuteQuery<T>(this dbContext db, string query) where T : class, new()
{
using (var command = db.Database.GetDbConnection().CreateCommand())
{
command.CommandText = query;
command.CommandType = CommandType.Text;
db.Database.OpenConnection();
using (var reader = command.ExecuteReader())
{
var lst = new List<T>();
var lstColumns = new T().GetType().GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).ToList();
while (reader.Read())
{
var newObject = new T();
for (var i = 0; i < reader.FieldCount; i++)
{
var name = reader.GetName(i);
PropertyInfo prop = lstColumns.FirstOrDefault(a => a.Name.ToLower().Equals(name.ToLower()));
if (prop == null)
{
continue;
}
var val = reader.IsDBNull(i) ? null : reader[i];
prop.SetValue(newObject, val, null);
}
lst.Add(newObject);
}
return lst;
}
}
}
用法:
var db = new dbContext();
string query = @"select ID , Name from People where ... ";
var lst = db.ExecuteQuery<PeopleView>(query);
我的模型:(不在DbSet
中):
public class PeopleView
{
public int ID { get; set; }
public string Name { get; set; }
}
在
.netCore 2.2 and 3.0
中測試。
注意:此解決方案性能較慢
在 Core 2.1 中,您可以執行以下操作:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Query<Ranks>();
}
然后定義你的 SQL 過程,比如:
public async Task<List<Ranks>> GetRanks(string value1, Nullable<decimal> value2)
{
SqlParameter value1Input = new SqlParameter("@Param1", value1?? (object)DBNull.Value);
SqlParameter value2Input = new SqlParameter("@Param2", value2?? (object)DBNull.Value);
List<Ranks> getRanks = await this.Query<Ranks>().FromSql("STORED_PROCEDURE @Param1, @Param2", value1Input, value2Input).ToListAsync();
return getRanks;
}
這樣 Ranks 模型將不會在您的數據庫中創建。
現在在您的控制器/動作中,您可以調用:
List<Ranks> gettingRanks = _DbContext.GetRanks(value1,value2).Result.ToListAsync();
這樣您就可以調用原始 SQL 過程。
我使用Dapper繞過了 Entity framework Core 的這個約束。
IDbConnection.Query
正在使用具有多個參數的 sql 查詢或存儲過程。 順便說一下,它有點快(見基准測試)
Dapper 很容易學習。 編寫和運行帶參數的存儲過程花了 15 分鍾。 無論如何,您可以同時使用 EF 和 Dapper。 下面是一個例子:
public class PodborsByParametersService
{
string _connectionString = null;
public PodborsByParametersService(string connStr)
{
this._connectionString = connStr;
}
public IList<TyreSearchResult> GetTyres(TyresPodborView pb,bool isPartner,string partnerId ,int pointId)
{
string sqltext "spGetTyresPartnerToClient";
var p = new DynamicParameters();
p.Add("@PartnerID", partnerId);
p.Add("@PartnerPointID", pointId);
using (IDbConnection db = new SqlConnection(_connectionString))
{
return db.Query<TyreSearchResult>(sqltext, p,null,true,null,CommandType.StoredProcedure).ToList();
}
}
}
不直接針對 OP 的場景,但由於我一直在努力解決這個問題,我想放棄這些前任。 使用DbContext
更容易執行原始 SQL 的方法:
public static class DbContextCommandExtensions
{
public static async Task<int> ExecuteNonQueryAsync(this DbContext context, string rawSql,
params object[] parameters)
{
var conn = context.Database.GetDbConnection();
using (var command = conn.CreateCommand())
{
command.CommandText = rawSql;
if (parameters != null)
foreach (var p in parameters)
command.Parameters.Add(p);
await conn.OpenAsync();
return await command.ExecuteNonQueryAsync();
}
}
public static async Task<T> ExecuteScalarAsync<T>(this DbContext context, string rawSql,
params object[] parameters)
{
var conn = context.Database.GetDbConnection();
using (var command = conn.CreateCommand())
{
command.CommandText = rawSql;
if (parameters != null)
foreach (var p in parameters)
command.Parameters.Add(p);
await conn.OpenAsync();
return (T)await command.ExecuteScalarAsync();
}
}
}
我的案例使用存儲過程而不是原始 SQL
創建了一個班級
Public class School
{
[Key]
public Guid SchoolId { get; set; }
public string Name { get; set; }
public string Branch { get; set; }
public int NumberOfStudents { get; set; }
}
在我的DbContext
類下面添加
public DbSet<School> SP_Schools { get; set; }
要執行存儲過程:
var MySchools = _db.SP_Schools.FromSqlRaw("GetSchools @schoolId, @page, @size ",
new SqlParameter("schoolId", schoolId),
new SqlParameter("page", page),
new SqlParameter("size", size)))
.IgnoreQueryFilters();
我從@AminRostami 更新了擴展方法以返回 IAsyncEnumerable(因此可以應用 LINQ 過濾),並且它將從數據庫返回的記錄的模型列名稱映射到模型(使用 EF Core 5 測試):
擴展本身:
public static class QueryHelper
{
private static string GetColumnName(this MemberInfo info)
{
List<ColumnAttribute> list = info.GetCustomAttributes<ColumnAttribute>().ToList();
return list.Count > 0 ? list.Single().Name : info.Name;
}
/// <summary>
/// Executes raw query with parameters and maps returned values to column property names of Model provided.
/// Not all properties are required to be present in model (if not present - null)
/// </summary>
public static async IAsyncEnumerable<T> ExecuteQuery<T>(
[NotNull] this DbContext db,
[NotNull] string query,
[NotNull] params SqlParameter[] parameters)
where T : class, new()
{
await using DbCommand command = db.Database.GetDbConnection().CreateCommand();
command.CommandText = query;
command.CommandType = CommandType.Text;
if (parameters != null)
{
foreach (SqlParameter parameter in parameters)
{
command.Parameters.Add(parameter);
}
}
await db.Database.OpenConnectionAsync();
await using DbDataReader reader = await command.ExecuteReaderAsync();
List<PropertyInfo> lstColumns = new T().GetType()
.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).ToList();
while (await reader.ReadAsync())
{
T newObject = new();
for (int i = 0; i < reader.FieldCount; i++)
{
string name = reader.GetName(i);
PropertyInfo prop = lstColumns.FirstOrDefault(a => a.GetColumnName().Equals(name));
if (prop == null)
{
continue;
}
object val = await reader.IsDBNullAsync(i) ? null : reader[i];
prop.SetValue(newObject, val, null);
}
yield return newObject;
}
}
}
使用的模型(請注意,列名稱與實際屬性名稱不同):
public class School
{
[Key] [Column("SCHOOL_ID")] public int SchoolId { get; set; }
[Column("CLOSE_DATE", TypeName = "datetime")]
public DateTime? CloseDate { get; set; }
[Column("SCHOOL_ACTIVE")] public bool? SchoolActive { get; set; }
}
實際使用:
public async Task<School> ActivateSchool(int schoolId)
{
// note that we're intentionally not returning "SCHOOL_ACTIVE" with select statement
// this might be because of certain IF condition where we return some other data
return await _context.ExecuteQuery<School>(
"UPDATE SCHOOL SET SCHOOL_ACTIVE = 1 WHERE SCHOOL_ID = @SchoolId; SELECT SCHOOL_ID, CLOSE_DATE FROM SCHOOL",
new SqlParameter("@SchoolId", schoolId)
).SingleAsync();
}
我在 github 上找到了 package EntityFrameworkCore.RawSQLExtensions 。要使用它,請添加 nuget package。
<PackageReference Include="EntityFrameworkCore.RawSQLExtensions" Version="1.2.0" />
該庫沒有記錄,但下面是我將它與 .NET 6 + EF Core 6 + Npgsql 6 一起使用
public class DbResult
{
public string Name { get; set; }
public int Age { get; set; }
}
using EntityFrameworkCore.RawSQLExtensions.Extensions;
var results = await context.Database
.SqlQuery<DbResult>(
@"select name, age from ""users"" where age > @Age",
new NpgsqlParameter("@Age", 15))
.ToListAsync();
該解決方案很大程度上依賴於@pius 的解決方案。 我想添加支持查詢參數的選項以幫助減輕 SQL 注入,並且我還想使其成為 Entity Framework Core 的 DbContext DatabaseFacade 的擴展,使其更加集成。
首先創建一個帶有擴展名的新類:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;
namespace EF.Extend
{
public static class ExecuteSqlExt
{
/// <summary>
/// Execute raw SQL query with query parameters
/// </summary>
/// <typeparam name="T">the return type</typeparam>
/// <param name="db">the database context database, usually _context.Database</param>
/// <param name="query">the query string</param>
/// <param name="map">the map to map the result to the object of type T</param>
/// <param name="queryParameters">the collection of query parameters, if any</param>
/// <returns></returns>
public static List<T> ExecuteSqlRawExt<T, P>(this DatabaseFacade db, string query, Func<DbDataReader, T> map, IEnumerable<P> queryParameters = null)
{
using (var command = db.GetDbConnection().CreateCommand())
{
if((queryParameters?.Any() ?? false))
command.Parameters.AddRange(queryParameters.ToArray());
command.CommandText = query;
command.CommandType = CommandType.Text;
db.OpenConnection();
using (var result = command.ExecuteReader())
{
var entities = new List<T>();
while (result.Read())
{
entities.Add(map(result));
}
return entities;
}
}
}
}
}
請注意,上面的“T”是返回的類型,“P”是查詢參數的類型,這將根據您使用的是 MySql、Sql 等而有所不同。
接下來我們將展示一個示例。 我正在使用 MySql EF Core 功能,因此我們將了解如何將上面的通用擴展與這個更具體的 MySql 實現一起使用:
//add your using statement for the extension at the top of your Controller
//with all your other using statements
using EF.Extend;
//then your your Controller looks something like this
namespace Car.Api.Controllers
{
//Define a quick Car class for the custom return type
//you would want to put this in it's own class file probably
public class Car
{
public string Make { get; set; }
public string Model { get; set; }
public string DisplayTitle { get; set; }
}
[ApiController]
public class CarController : ControllerBase
{
private readonly ILogger<CarController> _logger;
//this would be your Entity Framework Core context
private readonly CarContext _context;
public CarController(ILogger<CarController> logger, CarContext context)
{
_logger = logger;
_context = context;
}
//... more stuff here ...
/// <summary>
/// Get car example
/// </summary>
[HttpGet]
public IEnumerable<Car> Get()
{
//instantiate three query parameters to pass with the query
//note the MySqlParameter type is because I'm using MySql
MySqlParameter p1 = new MySqlParameter
{
ParameterName = "id1",
Value = "25"
};
MySqlParameter p2 = new MySqlParameter
{
ParameterName = "id2",
Value = "26"
};
MySqlParameter p3 = new MySqlParameter
{
ParameterName = "id3",
Value = "27"
};
//add the 3 query parameters to an IEnumerable compatible list object
List<MySqlParameter> queryParameters = new List<MySqlParameter>() { p1, p2, p3 };
//note the extension is now easily accessed off the _context.Database object
//also note for ExecuteSqlRawExt<Car, MySqlParameter>
//Car is my return type "T"
//MySqlParameter is the specific DbParameter type MySqlParameter type "P"
List<Car> result = _context.Database.ExecuteSqlRawExt<Car, MySqlParameter>(
"SELECT Car.Make, Car.Model, CONCAT_WS('', Car.Make, ' ', Car.Model) As DisplayTitle FROM Car WHERE Car.Id IN(@id1, @id2, @id3)",
x => new Car { Make = (string)x[0], Model = (string)x[1], DisplayTitle = (string)x[2] },
queryParameters);
return result;
}
}
}
該查詢將返回如下行:
“福特”、“探險者”、“福特探險者”
“特斯拉”、“X 型”、“特斯拉 X 型”
顯示標題未定義為數據庫列,因此默認情況下它不會是 EF Car 模型的一部分。 我喜歡這種方法作為許多可能的解決方案之一。 此頁面上的其他答案引用了使用 [NotMapped] 裝飾器解決此問題的其他方法,這取決於您的用例,這可能是更合適的方法。
請注意,此示例中的代碼顯然比它需要的更冗長,但我認為它使示例更清晰。
實際上你可以創建一個通用存儲庫並做這樣的事情
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : BaseEntity
{
private readonly DataContext context;
private readonly DbSet<TEntity> dbSet;
public GenericRepository(DataContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
public IEnumerable<TEntity> ExecuteCommandQuery(string command)
=> dbSet.FromSqlRaw(command);
}
您也可以使用QueryFirst 。 像 Dapper 一樣,這完全在 EF 之外。 與 Dapper(或 EF)不同,您不需要維護 POCO,您可以在真實環境中編輯您的 sql SQL,並且它會不斷地針對 DB 重新驗證。 免責聲明:我是 QueryFirst 的作者。
為 Entity Framework Core 5 完成此操作,需要安裝
Microsoft.EntityFrameworkCore.Relational
輔助擴展方法
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
public static class EfHelper
{
public static DbTransaction GetDbTransaction(this IDbContextTransaction source)
{
return (source as IInfrastructure<DbTransaction>).Instance;
}
private class PropertyMapp
{
public string Name { get; set; }
public Type Type { get; set; }
public bool IsSame(PropertyMapp mapp)
{
if (mapp == null)
{
return false;
}
bool same = mapp.Name == Name && mapp.Type == Type;
return same;
}
}
public static IEnumerable<T> FromSqlQuery<T>(this DbContext context, string query, params object[] parameters) where T : new()
{
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic;
List<PropertyMapp> entityFields = (from PropertyInfo aProp in typeof(T).GetProperties(flags)
select new PropertyMapp
{
Name = aProp.Name,
Type = Nullable.GetUnderlyingType(aProp.PropertyType) ?? aProp.PropertyType
}).ToList();
List<PropertyMapp> dbDataReaderFields = new List<PropertyMapp>();
List<PropertyMapp> commonFields = null;
using (var command = context.Database.GetDbConnection().CreateCommand())
{
if (command.Connection.State != ConnectionState.Open)
{
command.Connection.Open();
}
var currentTransaction = context.Database.CurrentTransaction;
if (currentTransaction != null)
{
command.Transaction = currentTransaction.GetDbTransaction();
}
command.CommandText = query;
if (parameters.Any())
{
command.Parameters.AddRange(parameters);
}
using (var result = command.ExecuteReader())
{
while (result.Read())
{
if (commonFields == null)
{
for (int i = 0; i < result.FieldCount; i++)
{
dbDataReaderFields.Add(new PropertyMapp { Name = result.GetName(i), Type = result.GetFieldType(i) });
}
commonFields = entityFields.Where(x => dbDataReaderFields.Any(d => d.IsSame(x))).Select(x => x).ToList();
}
var entity = new T();
foreach (var aField in commonFields)
{
PropertyInfo propertyInfos = entity.GetType().GetProperty(aField.Name);
var value = (result[aField.Name] == DBNull.Value) ? null : result[aField.Name]; //if field is nullable
propertyInfos.SetValue(entity, value, null);
}
yield return entity;
}
}
}
}
/*
* https://entityframeworkcore.com/knowledge-base/35631903/raw-sql-query-without-dbset---entity-framework-core
*/
public static IEnumerable<T> FromSqlQuery<T>(this DbContext context, string query, Func<DbDataReader, T> map, params object[] parameters)
{
using (var command = context.Database.GetDbConnection().CreateCommand())
{
if (command.Connection.State != ConnectionState.Open)
{
command.Connection.Open();
}
var currentTransaction = context.Database.CurrentTransaction;
if (currentTransaction != null)
{
command.Transaction = currentTransaction.GetDbTransaction();
}
command.CommandText = query;
if (parameters.Any())
{
command.Parameters.AddRange(parameters);
}
using (var result = command.ExecuteReader())
{
while (result.Read())
{
yield return map(result);
}
}
}
}
}
模型
public class UserModel
{
public string Name { get; set; }
public string Email { get; set; }
public bool? IsDeleted { get; set; }
}
手動映射
List<UserModel> usersInDb = Db.FromSqlQuery
(
"SELECT Name, Email FROM Users WHERE Name=@paramName",
x => new UserModel
{
Name = (string)x[0],
Email = (string)x[1]
},
new SqlParameter("@paramName", user.Name)
)
.ToList();
usersInDb = Db.FromSqlQuery
(
"SELECT Name, Email FROM Users WHERE Name=@paramName",
x => new UserModel
{
Name = x["Name"] is DBNull ? "" : (string)x["Name"],
Email = x["Email"] is DBNull ? "" : (string)x["Email"]
},
new SqlParameter("@paramName", user.Name)
)
.ToList();
使用反射的自動映射
List<UserModel> usersInDb = Db.FromSqlQuery<UserModel>
(
"SELECT Name, Email, IsDeleted FROM Users WHERE Name=@paramName",
new SqlParameter("@paramName", user.Name)
)
.ToList();
我知道這是一個老問題,但也許它可以幫助某人在不添加 DTO 作為 DbSet 的情況下調用存儲過程。
我提出這個問題是因為我們在 Entity Framework 6 中有 100 多個SqlQuery
的無實體使用實例,因此在我們的案例中采用 Microsoft 建議的方式根本不容易工作。
此外,在從EF
遷移到EFC
時,我們不得不將單個EF
(Entity Framework 6)/ EFC
(Entity Framework Core 5)代碼庫維護幾個月。 代碼庫相當大,根本不可能“一夜之間”遷移。
下面的答案是基於上面的很好的答案,它只是一個小的擴展,使它們適用於更多的邊緣情況。
首先,對於每個基於EF
的項目,我們創建了一個基於EFC
的項目(例如MyProject.csproj
==> MyProject_EFC.csproj
),並且在所有此類EFC
項目中,我們定義了一個常量EFCORE
。 如果您正在進行從EF
到EFC
的快速一次性遷移,那么您不需要這樣做,您可以保留#if EFCORE ... #else
中的內容並刪除下面#else ... #endif
中的內容。
這是主要的互操作擴展類。
using System;
using System.Collections.Generic;
using System.Threading;
#if EFCORE
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage;
using Database = Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade;
using MoreLinq.Extensions;
#else
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
#endif
namespace YourNameSpace.EntityFrameworkCore
{
/// <summary>
/// Collection of extension methods to simplify migration from EF to EFC.
/// </summary>
public static class EntityFrameworkCoreInterop
{
/// <summary>
/// https://stackoverflow.com/questions/6637679/reflection-get-attribute-name-and-value-on-property
/// </summary>
public static TAttribute? TryGetAttribute<TAttribute>(this PropertyInfo prop) where TAttribute : Attribute =>
prop.GetCustomAttributes(true).TryGetAttribute<TAttribute>();
public static TAttribute? TryGetAttribute<TAttribute>(this Type t) where TAttribute : Attribute =>
t.GetCustomAttributes(true).TryGetAttribute<TAttribute>();
public static TAttribute? TryGetAttribute<TAttribute>(this IEnumerable<object> attrs) where TAttribute : Attribute
{
foreach (object attr in attrs)
{
switch (attr)
{
case TAttribute t:
{
return t;
}
}
}
return null;
}
/// <summary>
/// Returns true if the source string matches *any* of the passed-in strings (case insensitive)
/// </summary>
public static bool EqualsNoCase(this string? s, params string?[]? targets)
{
if (s == null && (targets == null || targets.Length == 0))
{
return true;
}
if (targets == null)
{
return false;
}
return targets.Any(t => string.Equals(s, t, StringComparison.OrdinalIgnoreCase));
}
#if EFCORE
public class EntityException : Exception
{
public EntityException(string message) : base(message)
{
}
}
public static TEntity GetEntity<TEntity>(this EntityEntry<TEntity> entityEntry)
where TEntity : class => entityEntry.Entity;
#region SqlQuery Interop
/// <summary>
/// kk:20210727 - This is a little bit ugly but given that this interop method is used just once,
/// it is not worth spending more time on it.
/// </summary>
public static List<T> ToList<T>(this IOrderedAsyncEnumerable<T> e) =>
Task.Run(() => e.ToListAsync().AsTask()).GetAwaiter().GetResult();
private static string GetColumnName(this MemberInfo info) =>
info.GetCustomAttributes().TryGetAttribute<ColumnAttribute>()?.Name ?? info.Name;
/// <summary>
/// See: https://stackoverflow.com/questions/35631903/raw-sql-query-without-dbset-entity-framework-core
/// Executes raw query with parameters and maps returned values to column property names of Model provided.
/// Not all properties are required to be present in the model. If not present then they will be set to nulls.
/// </summary>
private static async IAsyncEnumerable<T> ExecuteQuery<T>(this Database database, string query, params object[] parameters)
{
await using DbCommand command = database.GetDbConnection().CreateCommand();
command.CommandText = query;
command.CommandType = CommandType.Text;
if (database.CurrentTransaction != null)
{
command.Transaction = database.CurrentTransaction.GetDbTransaction();
}
foreach (var parameter in parameters)
{
// They are supposed to be of SqlParameter type but are passed as objects.
command.Parameters.Add(parameter);
}
await database.OpenConnectionAsync();
await using DbDataReader reader = await command.ExecuteReaderAsync();
var t = typeof(T);
// TODO kk:20210825 - I do know that the code below works as we use it in some other place where it does work.
// However, I am not 100% sure that R# proposed version does. Check and refactor when time permits.
//
// ReSharper disable once CheckForReferenceEqualityInstead.1
if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{
t = Nullable.GetUnderlyingType(t)!;
}
var lstColumns = t
.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.ToList();
while (await reader.ReadAsync())
{
if (t.IsPrimitive || t == typeof(string) || t == typeof(DateTime) || t == typeof(Guid) || t == typeof(decimal))
{
var val = await reader.IsDBNullAsync(0) ? null : reader[0];
yield return (T) val!;
}
else
{
var newObject = Activator.CreateInstance<T>();
for (var i = 0; i < reader.FieldCount; i++)
{
var name = reader.GetName(i);
var val = await reader.IsDBNullAsync(i) ? null : reader[i];
var prop = lstColumns.FirstOrDefault(a => a.GetColumnName().EqualsNoCase(name));
if (prop == null)
{
continue;
}
prop.SetValue(newObject, val, null);
}
yield return newObject;
}
}
}
#endregion
public static DbRawSqlQuery<TElement> SqlQuery<TElement>(this Database database, string sql, params object[] parameters) =>
new(database, sql, parameters);
public class DbRawSqlQuery<TElement> : IAsyncEnumerable<TElement>
{
private readonly IAsyncEnumerable<TElement> _elements;
internal DbRawSqlQuery(Database database, string sql, params object[] parameters) =>
_elements = ExecuteQuery<TElement>(database, sql, parameters);
public IAsyncEnumerator<TElement> GetAsyncEnumerator(CancellationToken cancellationToken = new ()) =>
_elements.GetAsyncEnumerator(cancellationToken);
public async Task<TElement> SingleAsync() => await _elements.SingleAsync();
public TElement Single() => Task.Run(SingleAsync).GetAwaiter().GetResult();
public async Task<TElement> FirstAsync() => await _elements.FirstAsync();
public TElement First() => Task.Run(FirstAsync).GetAwaiter().GetResult();
public async Task<TElement?> SingleOrDefaultAsync() => await _elements.SingleOrDefaultAsync();
public async Task<int> CountAsync() => await _elements.CountAsync();
public async Task<List<TElement>> ToListAsync() => await _elements.ToListAsync();
public List<TElement> ToList() => Task.Run(ToListAsync).GetAwaiter().GetResult();
}
#endif
}
}
並且用法與以前的EF
用法沒有區別:
public async Task<List<int>> GetMyResults()
{
using var ctx = GetMyDbContext();
const string sql = "select 1 as Result";
return await ctx.GetDatabase().SqlQuery<int>(sql).ToListAsync();
}
其中GetMyDbContext
是一種獲取數據庫上下文的方法,而GetDatabase
是一個單行互操作,它為給定的 IMyDbContext 返回((DbContext)context).Database
IMyDbContext : DbContext
。 這是為了簡化同時進行的EF
/ EFC
操作。
這適用於原始類型(上面的示例)、實體、本地類(但不適用於匿名類)。 通過GetColumnName
支持列重命名,但是,......它已經在上面完成了。
用於查詢數據:沒有現有實體
string query = "SELECT r.Name as roleName, ur.roleId, u.Id as userId FROM dbo.AspNetUserRoles AS ur INNER JOIN dbo.AspNetUsers AS u ON ur.UserId = u.Id INNER JOIN dbo.AspNetRoles AS r ON ur.RoleId = r.Id ";
ICollection<object> usersWithRoles = new List<object>();
using (var command = _identityDBContext.Database.GetDbConnection().CreateCommand())
{
command.CommandText = query;
command.CommandType = CommandType.Text;
await _identityDBContext.Database.OpenConnectionAsync();
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
usersWithRoles.Add(new {
roleName = reader.GetFieldValueAsync<string>(0).Result,
roleId = reader.GetFieldValueAsync<string>(1).Result,
userId = reader.GetFieldValueAsync<string>(2).Result
});
}
}
}
詳細的:
[HttpGet]
[Route("GetAllUsersWithRoles")]
public async Task<IActionResult> GetAllUsersWithRoles()
{
string query = "SELECT r.Name as roleName, ur.roleId, u.Id as userId FROM dbo.AspNetUserRoles AS ur INNER JOIN dbo.AspNetUsers AS u ON ur.UserId = u.Id INNER JOIN dbo.AspNetRoles AS r ON ur.RoleId = r.Id ";
try
{
ICollection<object> usersWithRoles = new List<object>();
using (var command = _identityDBContext.Database.GetDbConnection().CreateCommand())
{
command.CommandText = query;
command.CommandType = CommandType.Text;
await _identityDBContext.Database.OpenConnectionAsync();
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
usersWithRoles.Add(new {
roleName = reader.GetFieldValueAsync<string>(0).Result,
roleId = reader.GetFieldValueAsync<string>(1).Result,
userId = reader.GetFieldValueAsync<string>(2).Result
});
}
}
}
return StatusCode(200, usersWithRoles); // Get all users
}
catch (Exception e)
{
return StatusCode(500, e);
}
}
結果如下所示:
[
{
"roleName": "admin",
"roleId": "7c9cb1be-e987-4ec1-ae4d-e4c9790f57d8",
"userId": "12eadc86-6311-4d5e-8be8-df30799df265"
},
{
"roleName": "user",
"roleId": "a0d5ef46-b1e6-4a53-91ce-9ff5959f1ed8",
"userId": "12eadc86-6311-4d5e-8be8-df30799df265"
},
{
"roleName": "user",
"roleId": "a0d5ef46-b1e6-4a53-91ce-9ff5959f1ed8",
"userId": "3e7cd970-8c52-4dd1-847c-f824671ea15d"
}
]
對於我遇到的同樣問題,Dipon Roy 的回答非常准確,我嘗試了其他嘗試解決同一問題的實現,但在查詢性能方面,這個總是贏(有幾毫秒的差異,但它贏了)。 這就是為什么我沒有看到在這種情況下修改 AppContext 的意義,在這種情況下,您只想使用數據庫引擎中的某些函數或視圖中的數據。
使用 Entity Framework 6,您可以執行如下操作
創建模態類為
Public class User
{
public int Id { get; set; }
public string fname { get; set; }
public string lname { get; set; }
public string username { get; set; }
}
執行 Raw DQL SQL 命令如下:
var userList = datacontext.Database.SqlQuery<User>(@"SELECT u.Id ,fname , lname ,username FROM dbo.Users").ToList<User>();
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.