Raw SQL Query without DbSet - Entity Framework Core

后端 未结 17 1195
眼角桃花
眼角桃花 2020-11-22 13:27

With Entity Framework Core removing dbData.Database.SqlQuery I can\'t find a solution to build a raw SQL Query for my full-text search query th

相关标签:
17条回答
  • 2020-11-22 13:43

    This solution leans heavily on the solution from @pius. I wanted to add the option to support query parameters to help mitigate SQL injection and I also wanted to make it an extension off of the DbContext DatabaseFacade for Entity Framework Core to make it a little more integrated.

    First create a new class with the extension:

    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;
                    }
                }
                    
            }
        }
    
    }
    

    Note in the above that "T" is the type for the return and "P" is the type of your query parameters which will vary based on if you are using MySql, Sql, so on.

    Next we will show an example. I'm using the MySql EF Core capability, so we'll see how we can use the generic extension above with this more specific MySql implementation:

    //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;
           }
        }
    }
    

    The query would return rows like:
    "Ford", "Explorer", "Ford Explorer"
    "Tesla", "Model X", "Tesla Model X"

    The display title is not defined as a database column, so it wouldn't be part of the EF Car model by default. I like this approach as one of many possible solutions. The other answers on this page reference other ways to address this issue with the [NotMapped] decorator, which depending on your use case could be the more appropriate approach.

    Note the code in this example is obviously more verbose than it needs to be, but I thought it made the example clearer.

    0 讨论(0)
  • 2020-11-22 13:46

    Add Nuget package - Microsoft.EntityFrameworkCore.Relational

    using Microsoft.EntityFrameworkCore;
    ...
    await YourContext.Database.ExecuteSqlCommandAsync("... @p0, @p1", param1, param2 ..)
    

    This will return the row numbers as an int

    See - https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.executesqlcommand?view=efcore-3.0

    0 讨论(0)
  • 2020-11-22 13:46

    try this: (create extension method)

    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;
                    }
                }
            }
    

    Usage:

    var db = new dbContext();
    string query = @"select ID , Name from People where ... ";
    var lst = db.ExecuteQuery<PeopleView>(query);
    

    my model: (not in DbSet):

    public class PeopleView
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }
    

    tested in .netCore 2.2 and 3.0.

    Note: this solution has the slow performance

    0 讨论(0)
  • 2020-11-22 13:47

    In EF Core you no longer can execute "free" raw sql. You are required to define a POCO class and a DbSet for that class. In your case you will need to define Rank:

    var ranks = DbContext.Ranks
       .FromSql("SQL_SCRIPT OR STORED_PROCEDURE @p0,@p1,...etc", parameters)
       .AsNoTracking().ToList();
    

    As it will be surely readonly it will be useful to include the .AsNoTracking() call.

    EDIT - Breaking change in EF Core 3.0:

    DbQuery() is now obsolete, instead DbSet() should be used (again). If you have a keyless entity, i.e. it don't require primary key, you can use HasNoKey() method:

    ModelBuilder.Entity<SomeModel>().HasNoKey()
    

    More information can be found here

    0 讨论(0)
  • 2020-11-22 13:49

    In Core 2.1 you can do something like this:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
           modelBuilder.Query<Ranks>();
    }
    

    and then define you SQL Procedure, like:

    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;
    }
    

    This way Ranks model will not be created in your DB.

    Now in your controller/action you can call:

    List<Ranks> gettingRanks = _DbContext.GetRanks(value1,value2).Result.ToListAsync();
    

    This way you can call Raw SQL Procedures.

    0 讨论(0)
  • 2020-11-22 13:51

    Not directly targeting the OP's scenario, but since I have been struggling with this, I'd like to drop these ex. methods that make it easier to execute raw SQL with the DbContext:

    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();
        }
      }
    }
    
    0 讨论(0)
提交回复
热议问题