Skip to content

Commit

Permalink
Merge pull request #3 from erenken/work/v1
Browse files Browse the repository at this point in the history
Updated readme
  • Loading branch information
erenken authored May 6, 2023
2 parents 2006031 + af46b22 commit 90d0980
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main

jobs:
build:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main

jobs:
build:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down
26 changes: 26 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/sample/QuerySample/bin/Debug/net7.0/QuerySample.dll",
"args": [],
"cwd": "${workspaceFolder}/sample/QuerySample",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}
41 changes: 41 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/sample/QuerySample/QuerySample.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/sample/QuerySample/QuerySample.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/sample/QuerySample/QuerySample.csproj"
],
"problemMatcher": "$msCompile"
}
]
}
2 changes: 1 addition & 1 deletion sample/QuerySample/Data/AddressBookDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace QuerySample.Data
{
public class AddressBookDbContext : DbContext
{
public DbSet<ContactEntity> Contacts { get; set; }
public DbSet<ContactEntity> Contacts { get; set; } = default!;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
Expand Down
218 changes: 218 additions & 0 deletions src/myNOC.EntityFramework.Query/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,221 @@
# 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](https://github.com/erenken/queryPattern/tree/main/sample/QuerySample)

## Setup and Configuration

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

```csharp
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.

```csharp
public interface IAddressBookContext : IQueryContext { }
```

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

```csharp
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.

```csharp
public interface IAddressBookContextRepository : IQueryRepository { }
```

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

```csharp
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<>`

```csharp
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.

```csharp
public ContactNameContains(string namePart)
```

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

```csharp
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.

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

### `IQueryScalar<>`

```csharp
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

```csharp
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.

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

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

You run a scalar query the same way.

```csharp
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`.

```csharp
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.

```csharp
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();
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<Copyright>Copyright © myNOC LLC 2023</Copyright>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<PackageProjectUrl>https://github.com/erenken/weatherlink</PackageProjectUrl>
<RepositoryUrl>https://github.com/erenken/weatherlink</RepositoryUrl>
<PackageProjectUrl>https://github.com/erenken/queryPattern</PackageProjectUrl>
<RepositoryUrl>https://github.com/erenken/queryPattern</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>

Expand Down

0 comments on commit 90d0980

Please sign in to comment.