myNOC.EntityFramework.Query 0.1.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package myNOC.EntityFramework.Query --version 0.1.2
                    
NuGet\Install-Package myNOC.EntityFramework.Query -Version 0.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="myNOC.EntityFramework.Query" Version="0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="myNOC.EntityFramework.Query" Version="0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="myNOC.EntityFramework.Query" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add myNOC.EntityFramework.Query --version 0.1.2
                    
#r "nuget: myNOC.EntityFramework.Query, 0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package myNOC.EntityFramework.Query@0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=myNOC.EntityFramework.Query&version=0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=myNOC.EntityFramework.Query&version=0.1.2
                    
Install as a Cake Tool

myNOC.EntityFramework.Query

Overview

A library used to create EntityFramework queries.

The concept is to create a concrete class that contains query written for a specific job.

This helps you keep your repositories focused on one entity. You no longer need to think if you are violating the SOLID pattern by having a function SecurityRoles in your Employee repository that is using an Employee and Security entity.

The other big advantage is the ability to write unit test against your queries. When I have written queries and written unit tests against them they would fully pass the unit tests, but them I would get runtime errors when it failed to convert to SQL. Using this method you can easily use EntityFramework's InMemoryDatabase and mock up test data. You can verify your complex queries will do what you expect them to do.

Instead you would have a EmployeeSecurityRoles concrete class that inherits from IQueryList<>.

Sample Application

QuerySample

Setup and Configuration

To use myNOC.EntityFramework.Query you will need to add it to your IServiceCollection.

services.AddQueryPattern();

This will register any classes in the AppDomain that implements IQueryContext or IQueryRepository

Setting Up QueryContext

You will need to setup a QueryContext for any DbContext you want the query pattern to use. The query pattern needs the DbContext because your queries may need access to multiple entities.

I recommend you first create an interface for your context.

public interface IAddressBookContext : IQueryContext { }

Then you need to create your context AddressBookContext that inherits from the abstract class QueryContext.

public class AddressBookContext : QueryContext, IAddressBookContext
{
    private DbContext? _dbContext = null;
    public override DbContext GetContext()
    {
        if (_dbContext == null)
            _dbContext = new AddressBookDbContext();

        return _dbContext;
    }
}

This allows you to use any DbContext you want. You could inject a DbContext you are already using in your application and return it in GetContext().

Setting Up QueryRepository

You will need a QueryRepository for your DbContext. Again I recommend your first create an interface for your repository.

public interface IAddressBookContextRepository : IQueryRepository { }

Then you need to create your repository AddressBookContextRepository that inherits from the abstract class QueryRepository.

public class AddressBookContextRepository : QueryRepository, IAddressBookContextRepository
{
    public AddressBookContextRepository(IAddressBookContext context) : base(context) { }
}

Create Queries

There are two types of queries.

  • Lists
  • Scalar

They return exactly what they say. One returns a list and the other returns a scalar value.

IQueryList<>

public class ContactNameContains : IQueryList<ContactModel>
{
    private readonly string _namePart;

    public ContactNameContains(string namePart)
    {
        _namePart = namePart;
    }

    public IQueryable<ContactModel> Query(IQueryContext context)
    {
        var persons = context.Set<ContactEntity>();
        var query = from p in persons
                    where p.Name.Contains(_namePart, StringComparison.InvariantCultureIgnoreCase)
                    select new ContactModel
                    {
                        Id = p.Id,
                        Name = p.Name,
                        DisplayName = $"{p.Id} - {p.Name}"
                    };

        return query;
    }
}

Your criteria is passed into the constructor.

public ContactNameContains(string namePart)

In the Query method you have access to your IQueryContext and in turn all of its entities.

var persons = context.Set<ContactEntity>();

You can then use persons in your query. You could just use the context.Set<ContactEntity>() in your query, but I find this cleaner.

You can then use the criteria that was passed into the constructor in your query.

where p.Name.Contains(_namePart, StringComparison.InvariantCultureIgnoreCase)

IQueryScalar<>

internal class ContactGetIdByName : IQueryScalar<int>
{
    private readonly string _name;

    public ContactGetIdByName(string name)
    {
        _name = name;
    }

    public async Task<int> GetScalar(IQueryContext context)
    {
        var persons = context.Set<ContactEntity>();
        var query = from p in persons
                    where p.Name.Contains(_name, StringComparison.InvariantCultureIgnoreCase)
                    select p.Id;

        return await query.FirstOrDefaultAsync();
    }
}

Just like IQueryList<> you pass in your criteria to the constructor.

Running Queries

IServiceCollection services = new ServiceCollection();
services.AddQueryPattern();

var provider = services.BuildServiceProvider();

var queryRepo = provider.GetRequiredService<IAddressBookContextRepository>();

You need to get an instance of your query repository IAddressBookContextRepository.

Once you have your queryRepo you can execute the query.

var result = await queryRepo.Query(new ContactNameContains("a"));

ContactNameContains inherits from IQueryList<ContactModel> so the Query method will return IEnumerable<ContactModel> where the name contains an a.

You run a scalar query the same way.

var id = await queryRepo.Query(new ContactGetIdByName("Bob"));

ContactGetIdByName inherits from IQueryScalar<int> so the Query method will return an int.

Testing

For testing in the sample application I used an InMemoryDatabase.

public class AddressBookDbContext : DbContext
{
    public DbSet<ContactEntity> Contacts { get; set; } = default!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("AddressBook");
        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

and seeded my data like so.

static async Task SeedSampleData()
{
	var addressBook = new AddressBookDbContext();
	addressBook.Add(new ContactEntity { Id = 1, Name = "Abby" });
	addressBook.Add(new ContactEntity { Id = 2, Name = "Bob" });
	addressBook.Add(new ContactEntity { Id = 3, Name = "Charlie" });
	addressBook.Add(new ContactEntity { Id = 4, Name = "David" });
	await addressBook.SaveChangesAsync();
}
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.4 177 12/31/2024
0.1.3 205 3/23/2024
0.1.2 237 5/7/2023
0.1.1 236 5/6/2023
0.1.0 260 4/17/2023