myNOC.EntityFramework.Query
0.1.4
dotnet add package myNOC.EntityFramework.Query --version 0.1.4
NuGet\Install-Package myNOC.EntityFramework.Query -Version 0.1.4
<PackageReference Include="myNOC.EntityFramework.Query" Version="0.1.4" />
<PackageVersion Include="myNOC.EntityFramework.Query" Version="0.1.4" />
<PackageReference Include="myNOC.EntityFramework.Query" />
paket add myNOC.EntityFramework.Query --version 0.1.4
#r "nuget: myNOC.EntityFramework.Query, 0.1.4"
#:package myNOC.EntityFramework.Query@0.1.4
#addin nuget:?package=myNOC.EntityFramework.Query&version=0.1.4
#tool nuget:?package=myNOC.EntityFramework.Query&version=0.1.4
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
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 | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.