3

Basically the problem i have is that i want to run a query in a database that it's not a representation of my model.

This is my code to create the connection to another database:

public static OtherContext GetNewContextGeneric(string connectionString)
        {
            var builder = new DbContextOptionsBuilder();
            builder.UseSqlServer(connectionString);

            OtherContext db = new OtherContext(builder.Options);

            return db;
        }

And this is my code to execute the query:

public List<IQueryble> Query (string connectionString, string query)
        {
            try
            {
                using(var contextGeneric = ContextFactory.GetNewContextGeneric(connectionString))
                {
                    //I want something like this
                    return contextGeneric.Query(query).ToList();
                }
            }
            catch(System.Data.SqlClient.SqlException ex)
            {
                throw new SQLIncorrectException(ex);
            }
            catch(System.InvalidOperationException ex)
            {
                throw new NotImplementedException();
            }   
        }

Can somebody help me?

Daniel Acevedo
  • 79
  • 1
  • 1
  • 11
  • Yes sorry for that – Daniel Acevedo Apr 20 '19 at 23:27
  • What query do you want to run? Are you getting records back? Or is it more of a fire-and-forget? – Erik Philips Apr 20 '19 at 23:54
  • I'm getting records yes, for example: SELECT Count(*) as Result FROM People p WHERE p.Age >=18; – Daniel Acevedo Apr 20 '19 at 23:55
  • You don't have to materialize models to do a count in EF..... you simply `int count = DbSet().Where( p => p.age >= 18).Count();` – Erik Philips Apr 20 '19 at 23:58
  • The query could be more difficult than that, it was just an example, another example could be: SELECT SUM(OrdersLine.LineQuantity) AS TotalVendidos FROM (Product INNER JOIN Item ON Product.ProductId = Item.ProductId) INNER JOIN OrdersLine ON Item.ItemId = OrdersLine.ItemId WHERE Product.CatId = 5) – Daniel Acevedo Apr 21 '19 at 00:00
  • For more complex querys. why wouldn't you want to create a model that represents what you're going to return? You can simply project the result into a model, and that model does not need to be any entity-framework model. – Erik Philips Apr 21 '19 at 00:01
  • Because i don't know how is created the database, i don't know what tables are in the database i want to insert the sql query. The only thing i know is the connection string for a new database totally independent from mine. That is way i can't create a model that represents the result of the query because i don't know what is going to return the query. – Daniel Acevedo Apr 21 '19 at 00:07
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/192157/discussion-between-erik-philips-and-daniel-acevedo). – Erik Philips Apr 21 '19 at 00:08
  • 1
    Possible duplicate of [Raw SQL Query without DbSet - Entity Framework Core](https://stackoverflow.com/questions/35631903/raw-sql-query-without-dbset-entity-framework-core) – ErikEJ Apr 21 '19 at 07:18
  • This is a very close duplicate the different is that this post is not about extending an existing context but querying an external DB, the same concepts from the dup should be used to solve this issue, with the minor changes to support a connection string being passed through rather than a `DbConnection`. – Chris Schaller Mar 07 '22 at 01:13

5 Answers5

3

You can use DbDataReader:

using (var command = context.Database.GetDbConnection().CreateCommand())
{
    command.CommandText = "SELECT * From Make";
    context.Database.OpenConnection();
    using (var reader = command.ExecuteReader())
    {
        // Do something with result
        reader.Read(); // Read first row
        var firstColumnObject = reader.GetValue(0);
        var secondColumnObject = reader.GetValue(1);

        reader.Read(); // Read second row
        firstColumnObject = reader.GetValue(0);
        secondColumnObject = reader.GetValue(1);
    }
}

Here you can learn more how to read values from DbDataReader.

Alternatively you could use FromSql() method, but that works only on predefined DbSet of some entity, which is not the solution you wanted.

Prolog
  • 2,698
  • 1
  • 18
  • 31
1

In the question you say:

Basically the problem i have is that i want to run a query in a database that it's not a representation of my model.

and then in comments you add:

Because i don't know how is created the database, i don't know what tables are in the database i want to insert the sql query

Well, if you don't know the database, then you cannot use Entity Framework, as it requires you to have a detailed knowledge of the database you are connecting to.
You should use plain ADO.NET (or Dapper if you want to map results back to a known class) for this.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • 1
    This is a very misleading response. EF doesn't require any more knowledge than you would need for ADO or Dapper, in fact EF requires _LESS_ knowledge if you generate your EF model from the database as the entire schema can be represented in concrete class definitions which will make it easier to work with and explore through intellisence prompts as you write your code and queries. – Chris Schaller Mar 18 '23 at 23:20
  • @ChrisSchaller How could EF require *less* knowledge? The OP said they don't have access to the DB to check which tables are there, so how could they ever create a model from the database? Read my answer a few more times and you'll understand. Furthermore, look at the OP's answer to see how they ignored EF completely. – Camilo Terevinto Mar 19 '23 at 08:49
  • 1
    With EF you only need a connection string, from there you can scaffold out the model from the DB. I say _less_ knowledge because now with a fully typed model in C# you can use LINQ to query and explore the database safely without writing a single SQL statement. To do this without EF (as OP has done) requires you to "know" the schema first, how else do you know what to write in your SQL statements. EF can provide that information for you, transferring the problem domain from SQL to object graphs in c# – Chris Schaller Mar 20 '23 at 20:50
1

Your use case of an external database can still be achieved using EF Core, there is no need to resort to ADO.Net, this solution is based on this generic solution for EF Core by @ErikEj. (Note that some functions and namespaces changed for EF Core 3, so and remain in .Net 5+ but the same generic concept can still be applied)

public static IList<T> Query<T>(string connectionString, string query, params object[] parameters) where T : class
{
    try
    {
        using (var contextGeneric = new ContextForQuery<T>(connectionString))
        {
            return contextGeneric.Query<T>().FromSql(query, parameters).ToList();
        }
    }
    catch (System.Data.SqlClient.SqlException ex)
    {
        throw new SQLIncorrectException(ex);
    }
    catch (System.InvalidOperationException ex)
    {
        throw new NotImplementedException();
    }
}

private class ContextForQuery<T> : DbContext where T : class
{
    private readonly string connectionString;

    public ContextForQuery(string connectionString)
    {
        this.connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(connectionString, options => options.EnableRetryOnFailure());

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<T>().HasNoKey();
        base.OnModelCreating(modelBuilder);
    }
}

Then the usage of this requires a concrete type definition, to add support for anonymous types is a fair bit more effort, but creating a concrete type for this is not a bad thing, the whole point here is to try you towards more declarative code styles as they enhance the readability and inspection of the code as well as providing documentation and other extended configuration like related entities.

public class NamedObject
{
    public int Id { get; set; }
    public string Name { get; set; }
}

...

var connectionString = "Insert your connection string here...";
var data = Query<NamedObject>(connectionString, "SELECT TOP 10 Id, FullName as Name FROM Employee");
foreach (var emp in data)
{
    Console.WriteLine(emp.Name);
}

Background

In EF 6 (.Net Framework) we could use DbContext.Database.FromSQL<T>() to execute ad-hoc SQL that would be automatically mapped to the specified type of T. This functionality was not replicated in EF Core because the result of FromSQL was inconsistent with the rest of EF, the result was a single use IEnumerable<T>. You could not further compose this query to Include() related entities nor could you add a filter to the underlying query.

In EF Core to Execute Raw SQL the type T that you want to return needs to be defined in the DbContext as a DbSet<T>. This set does not need to map to a table in the database at all, in fact since EF Core 2.1 we do not event need to specify a key for this type, it is simply a mechanism to pre-define the expected structure instead of executing Ad-Hoc requests on demand, it offers you the same functionality as the legacy FromSQL but also allows for you to define a rich set of navigation properties that would enable further composition of the query after your RawSQL is interpolated with the LINQ to SQL pipeline.

Once the type is defined in the context you simply call DbSet<T>.FromSqlRaw(). The difference is that now we have an IQueryable<T> that we can use to futher compose to include related entities or apply filters that will be evaluated within the database.

The solution posted in this response doesn't allow for composition, but uses the EF runtime in the expected sequence to give the same behaviours as the original EF 6 implementation.

In more recent versions of EF Core, and now in .Net 5+ the following subtle change need to be applied:

  • Core 2.1: return contextGeneric.Query<T>().FromSql(query, parameters).ToList();
  • Core 3+: return contextGeneric.Set<T>().FromSqlRaw(query, parameters).ToList();
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
  • 1
    Just one very minor comment: If you add the extension method Query to a class, the private class (ContextForQuery) must also be inside this same class. Private classes are not allowed as top-level classes inside a namespace in .net. I love this solution, btw. Thank you. – MPowerGuy Mar 17 '23 at 15:41
  • Absolutely @MPowerGuy I didn't really think to call that out at the time but you are correct. You will see private class implementations like this from time to time, in this case I have created a very special use `DbContext` implementation that I want to reserve the right to replace or modify for future versions of EF. If the class were made public there is a strong chance it would become a dependency on other logic that I don't want to be responsible for maintaining later. – Chris Schaller Mar 18 '23 at 00:02
  • For anyone who has followed some of my other responses, this is a variation of my personal solution to any time someone tries to argue against my premise that _"**EF** is better than **Dapper**"_. – Chris Schaller Mar 18 '23 at 00:04
  • I'm still trying to work out some of these generics syntaxes so pardon the question if it is too dumb: Should the signature of the extension method IList Query have the following as its first parameter: this DbContext db, ? As in: public static IList Query(this DbContext db, string connectionString, string query, params object[] parameters) where T : class ... or something else? I can't seem to find the Query method. – MPowerGuy Mar 19 '23 at 20:47
  • Ah never mind. I am dumb. It's just hanging off my Extensions class. Sorry. – MPowerGuy Mar 19 '23 at 20:52
0

You can use context.Database.ExecuteSqlRaw("select 1")

Don't forget to import the right namespace : using Microsoft.EntityFrameworkCore;

Kevin Dimey
  • 709
  • 6
  • 15
  • 1
    Looks like the OP wants to return a list. I don't think this is possible in EF Core with ExecuteSqlRaw, is it? – MPowerGuy Mar 17 '23 at 15:37
-3

It worked like this:

private void SqlCommand (string connectionString, string query)
        {
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                SqlCommand command = new SqlCommand(query, connection);
                connection.Open();
                SqlDataReader reader = command.ExecuteReader();
                try
                {
                    while (reader.Read())
                    {
                        var a = reader[0];
                    }
                }
                finally
                {
                    // Always call Close when done reading.
                    reader.Close();
                }
            }
        }

Or

using (var connection = ContextFactory.GetNewContextGeneric(connectionString).Database.GetDbConnection())
                {
                    connection.Open();
                    DbCommand command = connection.CreateCommand();
                    command.CommandText = query;

                    using (var reader = command.ExecuteReader())
                    {
                        // Do something with result
                        reader.Read(); // Read first row
                        var firstColumnObject = reader.GetValue(0);
                        /*var secondColumnObject = reader.GetValue(1);

                        reader.Read(); // Read second row
                        firstColumnObject = reader.GetValue(0);
                        secondColumnObject = reader.GetValue(1);*/
                        connection.Close();
                        return firstColumnObject.ToString();
                    }
                }
Daniel Acevedo
  • 79
  • 1
  • 1
  • 11