1

I have an Entity Framework Core statement where I need to create a union of multiple selects. The number of selects is variable.

I would like to do something like this, but it is not working:

public List<Valuation> GetValuations(List<GetValuationCriteria> valuationCriteriaList)
{
    IQueryable<Valuation> combinedQuery = null;
    IQueryable<Valuation> lastQuery = null;

    for (int xx = 0; xx < valuationCriteriaList.Count; xx++)
    {
        var valuationCriteria = valuationCriteriaList[xx];
        var selectQuery = DbContext.Valuations
            .Where(w => w.ActivityDate == valuationCriteria.ActivityDate &&
                w.ValuationSourceId == valuationCriteria.ValuationSourceId &&
                w.ActivityTypeId == valuationCriteria.ActivityTypeId &&
                w.CurrencyId == valuationCriteria.CurrencyId &&
                w.OrganizationHierarchyId == valuationCriteria.OrganizationHierarchyId &&
                w.StatusCode == "A");

        if (xx == 0)
        {
            combinedQuery = selectQuery;
            lastQuery = combinedQuery;
        }
        else
        {
            lastQuery.Concat(selectQuery);
            lastQuery = selectQuery;
        }
    }

    var result = combinedQuery.ToList();

    return result;
}

My goal is to execute the queries all in one trip to the database via the union. Is there a way to do this?

6
  • 1
    There's no Unioned query here. All LINQ operators return a new query but this code never uses that. In the end firstQuery is unchanged. You'd need something totalQuery=totalQuery.Union(query) BUT ... what are you actually trying to do? What are the real queries? EF is an ORM, not a SQL builder. It deals with entities. First, you can't just UNION queries targeting different tables for example. Where would the entity changes go? Second, UNIONs are expensive unless they can be simplified. If you want to query multiple names, you could use names.Contains(w.Name) to emit Name in (@id1,. Commented Jul 30 at 13:45
  • 1
    I already answered - 1) what is this? 2) totalQuery=totalQuery.Union(query) will actually produce a UNION, even if it's ineffective. If the partial queries are incompatible ... GOTO 1 - what is this and what are you actually trying to do? It matters a lot. You may be able to use a simple query instead of whatever you try to use now. Or specify an inheritance relation to load all the things you want at once. We can't guess from the current question - apart from the typo, it has no other information Commented Jul 30 at 13:49
  • Just edited with the real code. Commented Jul 30 at 13:55
  • 1
    .Concat creates a new IQueryable. You have to assign the result to something. Commented Jul 30 at 14:14
  • 1
    You need to actually use what Concat returns: combinedQuery=combinedQuery.Concat(selectQuery). What you try to do is equivalent to OR though and doesn't really need a UNION ALL. You could use LINQKit to combine multiple conditions with OR. BUT you should check performance. The query will be slow unless all fields are indexed to begin with. The database may or may not be able to simplify all those conditions in a way that allows all indexes to be used. Commented Jul 30 at 14:26

2 Answers 2

2

It looks like you just need records that match any of the criteria, so "criteria 1 or criteria 2 or ...".

So, what you could do is to generate single condition from each criteria object and then join all of them with OR operator. Then use such predicate in Where, which would just produce single query with WHERE clause.

While such generation of predicate could be implemented, there is good nuget package for doing this: LinqKit.

Example usage

Using below model classes:

public class Criteria
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class User
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

I have written such code generating said predicate:

var criteria = new Criteria[]
{
    new Criteria()
    {
        Name = "CriteriaName1",
        Email = "CriteriaEmail1",
    },
    new Criteria()
    {
        Name = "CriteriaName2",
        Email = "CriteriaEmail2",
    }
};

var predicate = PredicateBuilder.New<User>();

predicate = criteria.Aggregate(
    predicate,
    (predicate, criteria) => predicate
        .Or(x => x.Name == criteria.Name && x.Email == criteria.Email));

using var context = new MyContext();

Console.WriteLine(context.Users.Where(predicate).ToQueryString());

Then I could see single query with all criteria combined with OR:

DECLARE @__criteria_Name_0 nvarchar(4000) = N'CriteriaName1';
DECLARE @__criteria_Email_1 nvarchar(4000) = N'CriteriaEmail1';
DECLARE @__criteria_Name_2 nvarchar(4000) = N'CriteriaName2';
DECLARE @__criteria_Email_3 nvarchar(4000) = N'CriteriaEmail2';

SELECT [u].[Id], [u].[Email], [u].[Name]
FROM [Users] AS [u]
WHERE ([u].[Name] = @__criteria_Name_0 AND [u].[Email] = @__criteria_Email_1) OR ([u].[Name] = @__criteria_Name_2 AND [u].[Email] = @__criteria_Email_3)
Sign up to request clarification or add additional context in comments.

Comments

0

Your primary issue is this line

lastQuery.Concat(selectQuery);

It should say:

combinedQuery = lastQuery.Concat(selectQuery);

Although you could simplify the loop:

IQueryable<Valuation> combinedQuery = null;

foreach (var valuationCriteria in valuationCriteriaList)
{
    var selectQuery = DbContext.Valuations
            .Where(w => w.ActivityDate == valuationCriteria.ActivityDate &&
                w.ValuationSourceId == valuationCriteria.ValuationSourceId &&
                w.ActivityTypeId == valuationCriteria.ActivityTypeId &&
                w.CurrencyId == valuationCriteria.CurrencyId &&
                w.OrganizationHierarchyId == valuationCriteria.OrganizationHierarchyId &&
                w.StatusCode == "A");
    combinedQuery = combinedQuery is null
        ? selectQuery
        : combinedQuery.Concat(selectQuery);
}

But you would be better off using a Table Valued Parameter, rather than loads of UNION ALL queries.

First, create a Table Type

CREATE TYPE dbo.ValuationCriteria (
    ActivityDate date NOT NULL,
    ValuationSourceId int NOT NULL,
    ActivityTypeId int NOT NULL,
    CurrencyId char(3) NOT NULL,
    OrganizationHierarchyId int NOT NULL,
    -- what is the PRIMARY KEY ??
);

You need an entity class for this

class ValuationCriteria
{
    public DateTime ActivityDate { get; set; }
    public int ValuationSourceId { get; set; }
    public int ActivityTypeId { get; set; }
    public string CurrencyId { get; set; }
    public int OrganizationHierarchyId { get; set; }
}
modelBuilder.Entity<ValuationCriteria>().HasNoKey();

Then you create a DataTable with the values, and pass it in as a SqlParameter. You can then compose over that in a join.

public List<Valuation> GetValuations(List<GetValuationCriteria> valuationCriteriaList)
{
    var table = new DataTable { Columns = {
        { "ActivityDate", typeof(DateTime) },
        { "ValuationSourceId", typeof(int) },
        { "ActivityTypeId", typeof(int) },
        { "CurrencyId ", typeof(string) },
        { "OrganizationHierarchyId", typeof(int) },
    } };
    foreach (var criteria in valuationCriteriaList)
        table.Add(criteria.ActivityDate, criteria.ValuationSourceId, criteria.ActivityTypeId, criteria.CurrencyId, criteria.OrganizationHierarchyId);

    var param = new SqlParameter("@tvp", SqlDbType.Structured) { TypeName = "dbo.ValuationCriteria", Value = table };
    var tvp = DbContext.FromSql<ValuationCriteria>($"SELECT * FROM {param}");
    // must do this in a separate step or it won't compose
    var query = DbContext.Valuations
        .Where(w => tvp.Any(t =>
            w.ActivityDate == t.ActivityDate &&
            w.ValuationSourceId == t.ValuationSourceId &&
            w.ActivityTypeId == t.ActivityTypeId &&
            w.CurrencyId == t.CurrencyId &&
            w.OrganizationHierarchyId == t.OrganizationHierarchyId)
          && w.StatusCode == "A");
    return query.ToList();
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.