ardalis / Specification

Base class with tests for adding specifications to a DDD model
MIT License
1.83k stars 238 forks source link

Query to return anonymous type #365

Closed Jimmy-Ahmed closed 9 months ago

Jimmy-Ahmed commented 9 months ago

Hi,

I used the below code to project a property from my entity.

IQuerySpecification<Company, string> companyQuery = new QuerySpecification<Company, string>();
companyQuery.Query
            .Select(c => c.Name)
            .Where(c => c.Id == CompanyId);

Please advise how can I project anonymous type. Something like

IQuerySpecification<Company, string> companyQuery = new QuerySpecification<Company, string>();
companyQuery.Query
                     .Select(c => new { c.Id, c.Name })    // Build Error
                     .Where(c => c.Id == CompanyId);

Thanks

fiseni commented 9 months ago

Hi @Jimmy-Ahmed,

You may use object as the return type, EF works with runtime types and will be able to translate it properly. But, it all depends on what you plan to do with the result. If you just want to display or return it as a response, then it will work (as shown in the example below). Otherwise, if you plan to do further work against the result, then of course you won't be able to access the properties. For these scenarios, I'd strongly recommend to define a type. Now that we have records, it's oneliner anyway.

Here is a full sample

using Ardalis.Specification;
using Ardalis.Specification.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

await AppDbContext.SeedAsync();

using var dbContext = new AppDbContext();
var repo = new Repository<Customer>(dbContext);
var spec = new CustomerSpec(1);

var result = await repo.FirstOrDefaultAsync(spec);

Console.WriteLine($"Result: {result}");

public class CustomerSpec : Specification<Customer, object>
{
    public CustomerSpec(int id)
    {
        Query.Where(c => c.Id == id);
        Query.Select(c => new { c.Id, c.Name });
    }
}
public interface IRepository<T> : IRepositoryBase<T> where T : class
{
}
public class Repository<T> : RepositoryBase<T>, IRepository<T> where T : class
{
    public Repository(AppDbContext dbContext) : base(dbContext)
    {
    }
}
public record Customer
{
    public int Id { get; set; }
    public string? Name { get; set; }
}
public class AppDbContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connectionString = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SpecAnonymousTypes;Integrated Security=SSPI;ConnectRetryCount=0;";
        optionsBuilder.UseSqlServer(connectionString).LogTo(Console.WriteLine, LogLevel.Information);
    }

    public static async Task SeedAsync()
    {
        using var dbContext = new AppDbContext();
        await dbContext.Database.EnsureDeletedAsync();
        await dbContext.Database.EnsureCreatedAsync();
        var customers = new List<Customer>
        {
            new() {Name = "Customer1"},
            new() {Name = "Customer2"},
            new() {Name = "Customer3"},
        };
        dbContext.Customers.AddRange(customers);
        await dbContext.SaveChangesAsync();
    }
}

And here is the output. Let us know if you have further questions.

image

Jimmy-Ahmed commented 9 months ago

Thanks a lot for your quick response, Do you have an example for using records?

fiseni commented 9 months ago

Sure, just add the type in the previous example

using var dbContext = new AppDbContext();
var repo = new Repository<Customer>(dbContext);
var spec = new CustomerSpec(1);

var result = await repo.FirstOrDefaultAsync(spec);

Console.WriteLine($"Result: {result}");

public record CustomerDto(int Id, string? Name);

public class CustomerSpec : Specification<Customer, CustomerDto>
{
    public CustomerSpec(int id)
    {
        Query.Where(c => c.Id == id);
        Query.Select(c => new CustomerDto(c.Id, c.Name));
    }
}

Btw, doesn't have to be a record, it can be a class. I just mentioned it as an example, since it has a simple syntax.

Jimmy-Ahmed commented 9 months ago

Thanks a lot! :)