Skip to content

Commit a03c00c

Browse files
committed
Multiple enhancements
1 parent b66ceb3 commit a03c00c

File tree

10 files changed

+532
-41
lines changed

10 files changed

+532
-41
lines changed

NHUnitExample/Controllers/TestController.cs

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
using Microsoft.AspNetCore.Mvc;
22
using Microsoft.Extensions.Logging;
3+
using NHibernate.Util;
34
using NHUnitExample.Entities;
45
using System;
5-
using System.Collections;
66
using System.Collections.Generic;
77
using System.Linq;
88
using System.Threading;
99
using System.Threading.Tasks;
10-
using NHibernate.Util;
10+
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
1111
using Color = NHUnitExample.Entities.Color;
1212

1313
namespace NHUnitExample.Controllers
1414
{
1515
[Route("api/[controller]")]
1616
[ApiController]
17-
public class TestController : ControllerBase
17+
public class TestNHUnitController : ControllerBase
1818
{
19-
private readonly ILogger<TestController> _logger;
19+
private readonly ILogger<TestNHUnitController> _logger;
2020
private readonly IDbContext _dbContext;
2121

22-
public TestController(ILogger<TestController> logger, IDbContext dbContext)
22+
public TestNHUnitController(ILogger<TestNHUnitController> logger, IDbContext dbContext)
2323
{
2424
_logger = logger;
2525
_dbContext = dbContext;
2626
}
2727

2828
/// <summary>
2929
/// You need to setup the database before testing any other endpoint.
30-
/// At every project start the DB schema is recreated: Check Startup.cs -> BuildSchema(Configuration config)
30+
/// At every project start the DB schema is recreated: Check Startup.cs - BuildSchema(Configuration config)
3131
/// </summary>
3232
/// <param name="cancellationToken"></param>
3333
/// <returns></returns>
@@ -186,6 +186,33 @@ public async Task<IActionResult> GetAllForCustomer(CancellationToken cancellatio
186186
return Ok(customer);
187187
}
188188

189+
190+
/// <summary>
191+
/// A simple example for loading a list of entities by id with all the nested children.
192+
/// It's similar to IRepository.Get(id), the only difference is that it expects a collection of ids
193+
/// </summary>
194+
/// <param name="token"></param>
195+
/// <param name="customerIds"></param>
196+
/// <returns></returns>
197+
[HttpGet("/getAllForCustomerIds")]
198+
public async Task<IActionResult> GetAllForCustomerIds(CancellationToken token, List<int> customerIds = null)
199+
{
200+
if (customerIds?.Count == 0)
201+
{
202+
customerIds = new List<int>() { 11, 12 };
203+
}
204+
205+
//get the customer with all nested data and execute the above query too
206+
var customer = await _dbContext.Customers.GetMany(customerIds) //returns a wrapper to configure the query
207+
.Include(c => c.Addresses.Single().Country, //include Addresses in the result using the fastest approach: join, new query, batch fetch. Usage: c.Child1.ChildList2.Single().Child3 which means include Child1 and ChildList2 with ALL Child3 properties
208+
c => c.PhoneNumbers.Single().PhoneNumberType, //include all PhoneNumbers with PhoneNumberType
209+
c => c.Cart.Products.Single().Colors) //include Cart with Products and details about products
210+
.Unproxy() //instructs the framework to strip all the proxy classes when the Value is returned
211+
.ListAsync(token); //this is where the query(s) get executed
212+
213+
return Ok(customer);
214+
}
215+
189216
/// <summary>
190217
/// A simple example of projecting a query
191218
/// </summary>
@@ -275,14 +302,26 @@ public async Task<IActionResult> TestMultipleQueries(CancellationToken cancellat
275302
});
276303
}
277304

305+
/// <summary>
306+
/// Increase price by 5 for all products that are cheaper than 100 without loading them in memory.
307+
/// </summary>
308+
/// <param name="cancellationToken"></param>
309+
/// <returns></returns>
310+
[HttpGet("/testConditionalUpdate")]
311+
public async Task<IActionResult> TestConditionalUpdate(CancellationToken cancellationToken)
312+
{
313+
var productCount = _dbContext.Products.UpdateWhereAsync(p => p.Price < 100, p => new { Price = p.Price + 5 }, cancellationToken);
314+
return Ok($"Updated {productCount} products.");
315+
}
316+
278317
/// <summary>
279318
/// Execute your own SQL script using: ExecuteListAsync, ExecuteScalarAsync, ExecuteNonQueryAsync
280319
/// </summary>
281320
/// <param name="cancellationToken"></param>
282321
/// <param name="customerId"></param>
283322
/// <returns></returns>
284-
[HttpGet("/testSqlQuery")]
285-
public async Task<IActionResult> TestSqlQuery(CancellationToken cancellationToken, int customerId = 11)
323+
[HttpGet("/testScalarQuerySql")]
324+
public async Task<IActionResult> TestScalarQuerySql(CancellationToken cancellationToken, int customerId = 11)
286325
{
287326
var sqlQuery = @"select Id as CustomerId,
288327
Concat(FirstName,' ',LastName) as FullName,
@@ -293,6 +332,34 @@ public async Task<IActionResult> TestSqlQuery(CancellationToken cancellationToke
293332
return Ok(customResult);
294333
}
295334

335+
/// <summary>
336+
/// Execute your own SQL script to get all customers prices using ExecuteListAsync
337+
/// </summary>
338+
/// <param name="cancellationToken"></param>
339+
/// <returns></returns>
340+
[HttpGet("/testListQuerySql")]
341+
public async Task<IActionResult> TestListQuerySql(CancellationToken cancellationToken)
342+
{
343+
var sqlQuery = @"select Id, FirstName, LastName, BirthDate from ""Customer""";
344+
var customers = await _dbContext.ExecuteListAsync<Customer>(sqlQuery, null, cancellationToken);
345+
return Ok(customers);
346+
}
347+
348+
349+
/// <summary>
350+
/// Execute your own SQL script to update product prices using ExecuteNonQueryAsync
351+
/// </summary>
352+
/// <param name="price"></param>
353+
/// <param name="cancellationToken"></param>
354+
/// <returns></returns>
355+
[HttpGet("/testNonQuerySql")]
356+
public async Task<IActionResult> TestNonQuerySql(CancellationToken cancellationToken, int price = 5)
357+
{
358+
var sqlQuery = @"update ""Product"" set price=price+1 where price< :productPrice";
359+
await _dbContext.ExecuteNonQueryAsync(sqlQuery, new { productPrice = price }, cancellationToken);
360+
return Ok();
361+
}
362+
296363

297364
/// <summary>
298365
/// Execute a procedure or multiple queries which return multiple results.
@@ -316,9 +383,9 @@ public async Task<IActionResult> TestDataSetQuery(CancellationToken cancellation
316383
typeof(SqlQueryCustomResult), //first result type
317384
typeof(long));//second result type
318385

319-
//The results are returned in order in it's own collection.
386+
//The results are returned in order in their own collection.
320387
var customer = (SqlQueryCustomResult)customResult[0].FirstOrDefault(); //we might not have any results
321-
var customerCount = (long)customResult[1].First(); //the time must match: it will fail with int
388+
var customerCount = (long)customResult[1].First(); //the type must match: it will fail with int
322389

323390
return Ok(new
324391
{

NHUnitExample/NHUnitExample.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
<!--<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>-->
66
</PropertyGroup>
77

8+
<PropertyGroup>
9+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
10+
<NoWarn>$(NoWarn);1591</NoWarn>
11+
</PropertyGroup>
12+
813
<ItemGroup>
914
<PackageReference Include="FluentNHibernate" Version="2.1.2" />
1015
<PackageReference Include="Microsoft.AspNetCore.App" />

NHUnitExample/NHUnitExample.xml

Lines changed: 73 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

NHUnitExample/Startup.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
using NHibernate.Cfg;
1414
using NHibernate.Dialect;
1515
using NHibernate.Tool.hbm2ddl;
16-
using NHUnit;
1716
using Serilog;
1817
using Swashbuckle.AspNetCore.Swagger;
18+
using System;
19+
using System.IO;
20+
using System.Reflection;
21+
using Environment = NHibernate.Cfg.Environment;
1922
using ILoggerFactory = Microsoft.Extensions.Logging.ILoggerFactory;
2023

2124
namespace NHUnitExample
@@ -55,9 +58,13 @@ public void ConfigureServices(IServiceCollection services)
5558
{
5659
Version = "v1",
5760
Title = "FluentNHibernate Test",
58-
Description = "Test NHibernate & FluentNHibernate on .netcore",
61+
Description = "Test NHUnit with FluentNHibernate on .NET Core",
5962
TermsOfService = "None"
6063
});
64+
// Set the comments path for the Swagger JSON and UI.
65+
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
66+
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
67+
c.IncludeXmlComments(xmlPath);
6168
});
6269

6370
services.AddSingleton<ISessionFactory>(CreateSessionFactory());

README.md

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public DbContext(ISessionFactory sessionFactory) : base(sessionFactory) {
6969

7070
## Eager loading
7171
Using lambda expressions you can specify which child properties should be populated. The framework will determine the fastest approach to load the data: using join, future queries or by using batch fetching.
72-
Depending on the number of returned rows and the depth you might need to finetune your queries, but in most of the cases the framework takes the best decision.
72+
Depending on the number of returned rows and the depth you might need to finetune your queries, but in most cases the framework takes the best decision.
7373

7474
```csharp
7575
var customer = await _dbContext.Customers.Get(customerId) //returns a wrapper to configure the query
@@ -85,7 +85,7 @@ The expression `c.Addresses.Single().Country` will load all the nested child obj
8585

8686

8787
## Unproxy
88-
You can use IUnitOfWork.Unproxy, ISingleEntityWrapper.Unproxy or IEntityListWrapper.Unproxy
88+
You can use Unproxy from IUnitOfWork, ISingleEntityWrapper, IMultipleEntityWrapper or IEntityListWrapper
8989

9090
```csharp
9191
var customer = await _dbContext.Customers
@@ -98,23 +98,46 @@ An unproxied object will contain the foreign key ids (One to One and Many to One
9898
For example in the above query the Cart property will be instantiated and popuplated with the Id field.
9999

100100

101-
## Multi Queries
101+
## Deferred Execution
102102
Using `Deferred()` you can execute multiple queries in a single server trip.
103103
Bellow is an example with 3 different queries that get executed in one server trip.
104104

105105
```csharp
106106
//product count future
107107
var prodCountP = _dbContext.WrapQuery(_dbContext.Products.All())
108-
.Count().Deferred();
108+
.Count()
109+
.Deferred();
109110

110111
//most expensive 10 products future
111112
var expProdsP = _dbContext.WrapQuery(_dbContext.Products.All().OrderByDescending(p => p.Price).Take(10))
112113
.Deferred();
113114

114115
//get customer by id - executes all queries
115-
var customer = await _dbContext.Customers.Get(customerId).Deferred().ValueAsync();
116-
var prodCount = await prodCountP.ValueAsync(); //returns value
117-
var expProds = await expProdsP.ListAsync(); //returns value
116+
var customer = await _dbContext.Customers
117+
.Get(customerId)
118+
.Deferred()
119+
.ValueAsync();
120+
var prodCount = await prodCountP.ValueAsync(); //returns one value
121+
var expProds = await expProdsP.ListAsync(); //returns list
122+
```
123+
124+
125+
## Conditional Queries
126+
If you need to update or delete a set of records that meet a condition, you don't need to load them in memory. Just write an expression and it will be evaluated immediately.
127+
You should always run your commands inside a transaction with `BeginTransaction()` and `CommitTransactionAsync`.
128+
129+
### UpdateWhereAsync
130+
Increase price by 5 for all products that are less than 100, without loading them in memory.
131+
132+
```csharp
133+
await _dbContext.Products.UpdateWhereAsync(p => p.Price < 100, p => new { Price = p.Price + 5 });
134+
```
135+
136+
### DeleteWhereAsync
137+
Delete products that are too cheap, without loading them in memory.
138+
139+
```csharp
140+
await _dbContext.Products.DeleteWhereAsync(p => p.Price < 2);
118141
```
119142

120143

@@ -128,11 +151,15 @@ IUnitOfWork exposes:
128151

129152

130153
## Procedures/Queries
131-
In some rare cases you need to execute your own queries and NHUnit provides this functionality:
132-
- ExecuteListAsync
133-
- ExecuteScalarAsync
134-
- ExecuteNonQueryAsync
135-
- ExecuteMultipleQueries
154+
In some rare cases you need to execute your own queries/procedures and NHUnit provides this functionality:
155+
- ExecuteListAsync : return a list of values
156+
157+
```csharp
158+
var sqlQuery = @"select Id, FirstName, LastName, BirthDate from ""Customer""";
159+
var customers = await _dbContext.ExecuteListAsync<Customer>(sqlQuery, null, cancellationToken);
160+
```
161+
162+
- ExecuteScalarAsync : return single value/object
136163

137164
```csharp
138165
var sqlQuery = @"select Id as CustomerId,
@@ -141,4 +168,32 @@ var sqlQuery = @"select Id as CustomerId,
141168
from ""Customer""
142169
where Id= :customerId";
143170
var customResult = await _dbContext.ExecuteScalarAsync<SqlQueryCustomResult>(sqlQuery, new { customerId = 11 });
171+
```
172+
173+
- ExecuteNonQueryAsync : no value returned
174+
175+
```csharp
176+
var sqlQuery = @"update ""Product"" set price=price+1 where price< :productPrice";
177+
await _dbContext.ExecuteNonQueryAsync(sqlQuery, new { productPrice = 5 }, cancellationToken);
178+
```
179+
180+
- ExecuteMultipleQueriesAsync : return multiple result sets
181+
182+
```csharp
183+
var sqlQuery = @"select Id as CustomerId,
184+
Concat(FirstName,' ',LastName) as FullName,
185+
BirthDate
186+
from ""Customer""
187+
where Id= :customerId;
188+
189+
select count(*) Count from ""Customer"";";
190+
var customResult = await _dbContext.ExecuteMultipleQueriesAsync(sqlQuery, //query
191+
new { customerId }, //parameters: property name must be the same as the parameter
192+
cancellationToken,
193+
typeof(SqlQueryCustomResult), //first result type
194+
typeof(long));//second result type
195+
196+
//The results are returned in order in their own collection.
197+
var customer = (SqlQueryCustomResult)customResult[0].FirstOrDefault(); //we might not have any results
198+
var customerCount = (long)customResult[1].First(); //the type must match
144199
```

0 commit comments

Comments
 (0)