Skip to content

Commit b66ceb3

Browse files
committed
Add support to execute stored procedure with multiple result sets: ExecuteMultipleQueriesAsync
1 parent 82a24db commit b66ceb3

File tree

6 files changed

+213
-36
lines changed

6 files changed

+213
-36
lines changed

NHUnitExample/Controllers/TestController.cs

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
using Microsoft.Extensions.Logging;
33
using NHUnitExample.Entities;
44
using System;
5+
using System.Collections;
56
using System.Collections.Generic;
67
using System.Linq;
78
using System.Threading;
89
using System.Threading.Tasks;
10+
using NHibernate.Util;
911
using Color = NHUnitExample.Entities.Color;
1012

1113
namespace NHUnitExample.Controllers
@@ -23,8 +25,14 @@ public TestController(ILogger<TestController> logger, IDbContext dbContext)
2325
_dbContext = dbContext;
2426
}
2527

28+
/// <summary>
29+
/// 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)
31+
/// </summary>
32+
/// <param name="cancellationToken"></param>
33+
/// <returns></returns>
2634
[HttpGet("/initializeDatabase")]
27-
public async Task<IActionResult> InitializeDatabase(CancellationToken token)
35+
public async Task<IActionResult> InitializeDatabase(CancellationToken cancellationToken)
2836
{
2937
if (_dbContext.Countries.All().Any())
3038
return Ok("Already initialized");
@@ -39,7 +47,7 @@ public async Task<IActionResult> InitializeDatabase(CancellationToken token)
3947
new Country(){ Name = "India"},
4048
new Country(){ Name = "Romania"},
4149
};
42-
await _dbContext.Countries.InsertManyAsync(countries, token);
50+
await _dbContext.Countries.InsertManyAsync(countries, cancellationToken);
4351

4452
//colors
4553
var colors = new List<Color>
@@ -51,7 +59,7 @@ public async Task<IActionResult> InitializeDatabase(CancellationToken token)
5159
new Color(){ Name = "Blue"},
5260
new Color(){ Name = "Grey"},
5361
};
54-
await _dbContext.Colors.InsertManyAsync(colors, token);
62+
await _dbContext.Colors.InsertManyAsync(colors, cancellationToken);
5563

5664
//phone number types
5765
var phoneNumberTypes = new List<PhoneNumberType>
@@ -60,8 +68,8 @@ public async Task<IActionResult> InitializeDatabase(CancellationToken token)
6068
new PhoneNumberType(){ Name = "Mobile"},
6169
new PhoneNumberType(){ Name = "Business"},
6270
};
63-
await _dbContext.PhoneNumberTypes.InsertManyAsync(phoneNumberTypes, token);
64-
await _dbContext.SaveChangesAsync(token); // save changes but do not commit transaction (not required in this case)
71+
await _dbContext.PhoneNumberTypes.InsertManyAsync(phoneNumberTypes, cancellationToken);
72+
await _dbContext.SaveChangesAsync(cancellationToken); // save changes but do not commit transaction (not required in this case)
6573

6674
var random = new Random(15);
6775

@@ -75,7 +83,7 @@ public async Task<IActionResult> InitializeDatabase(CancellationToken token)
7583
Price = random.Next(100, 10000) / 100m
7684
}
7785
).ToList();
78-
await _dbContext.Products.InsertManyAsync(products, token);
86+
await _dbContext.Products.InsertManyAsync(products, cancellationToken);
7987

8088
//customers
8189
var customer = new Customer()
@@ -103,7 +111,7 @@ public async Task<IActionResult> InitializeDatabase(CancellationToken token)
103111
.ToList()
104112
.ForEach(p => cart.Products.Add(p));
105113
cart.LastUpdated = DateTime.UtcNow;
106-
await _dbContext.Customers.InsertAsync(customer, token);
114+
await _dbContext.Customers.InsertAsync(customer, cancellationToken);
107115

108116
//customer 2 - empty cart
109117
var customer2 = new Customer()
@@ -125,7 +133,7 @@ public async Task<IActionResult> InitializeDatabase(CancellationToken token)
125133
PhoneNumberType = phoneNumberTypes.First()
126134
});
127135
customer2.SetCart(new CustomerCart());
128-
await _dbContext.Customers.InsertAsync(customer2, token);
136+
await _dbContext.Customers.InsertAsync(customer2, cancellationToken);
129137

130138
//customer 3 - basic info
131139
var customer3 = new Customer()
@@ -134,39 +142,57 @@ public async Task<IActionResult> InitializeDatabase(CancellationToken token)
134142
FirstName = "Mike",
135143
LastName = "Golden"
136144
};
137-
await _dbContext.Customers.InsertAsync(customer3, token);
145+
await _dbContext.Customers.InsertAsync(customer3, cancellationToken);
138146

139-
await _dbContext.SaveChangesAsync(token);
140-
await _dbContext.CommitTransactionAsync(token);
147+
await _dbContext.SaveChangesAsync(cancellationToken);
148+
await _dbContext.CommitTransactionAsync(cancellationToken);
141149
return Ok("Initialization complete");
142150
}
143151

152+
/// <summary>
153+
/// A simple example of query wrap. You can join multiple entity types
154+
/// </summary>
155+
/// <param name="cancellationToken"></param>
156+
/// <returns></returns>
144157
[HttpGet("/getAllCustomers")]
145-
public async Task<IActionResult> GetAllCustomer(CancellationToken token)
158+
public async Task<IActionResult> GetAllCustomer(CancellationToken cancellationToken)
146159
{
147160
var customers = await _dbContext.WrapQuery(
148161
_dbContext.Customers.All())
149162
.Unproxy() //without this you might download the whole DB
150-
.ListAsync(token);
163+
.ListAsync(cancellationToken);
151164
return Ok(customers);
152165
}
153166

167+
/// <summary>
168+
/// A simple example for loading an entity with all the nested children.
169+
/// Behind the scenes it's using join, new future queries or batch fetching.
170+
/// At the end it's stripping all the NHibernate proxy classes so you don't need to worry about casting or unwanted lazy loading.
171+
/// </summary>
172+
/// <param name="cancellationToken"></param>
173+
/// <param name="customerId"></param>
174+
/// <returns></returns>
154175
[HttpGet("/getAllForCustomer")]
155-
public async Task<IActionResult> GetAllForCustomer(CancellationToken token, int customerId = 11)
176+
public async Task<IActionResult> GetAllForCustomer(CancellationToken cancellationToken, int customerId = 11)
156177
{
157178
//get the customer with all nested data and execute the above query too
158179
var customer = await _dbContext.Customers.Get(customerId) //returns a wrapper to configure the query
159180
.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
160181
c => c.PhoneNumbers.Single().PhoneNumberType, //include all PhoneNumbers with PhoneNumberType
161182
c => c.Cart.Products.Single().Colors) //include Cart with Products and details about products
162183
.Unproxy() //instructs the framework to strip all the proxy classes when the Value is returned
163-
.ValueAsync(token); //this is where the query(s) get executed
184+
.ValueAsync(cancellationToken); //this is where the query(s) get executed
164185

165186
return Ok(customer);
166187
}
167188

189+
/// <summary>
190+
/// A simple example of projecting a query
191+
/// </summary>
192+
/// <param name="cancellationToken"></param>
193+
/// <returns></returns>
168194
[HttpGet("/testProjections")]
169-
public async Task<IActionResult> TestProjections(CancellationToken token)
195+
public async Task<IActionResult> TestProjections(CancellationToken cancellationToken)
170196
{
171197
int pageNumber = 3;
172198
int pageSize = 10;
@@ -181,7 +207,7 @@ public async Task<IActionResult> TestProjections(CancellationToken token)
181207
Colors = p.Colors.Select(c => c.Name)
182208
});
183209

184-
var pagedProducts = await _dbContext.WrapQuery(query).ListAsync(token);
210+
var pagedProducts = await _dbContext.WrapQuery(query).ListAsync(cancellationToken);
185211

186212
return Ok(new
187213
{
@@ -192,8 +218,13 @@ public async Task<IActionResult> TestProjections(CancellationToken token)
192218
});
193219
}
194220

221+
/// <summary>
222+
/// Execute multiple queries in a single server trip.
223+
/// </summary>
224+
/// <param name="cancellationToken"></param>
225+
/// <returns></returns>
195226
[HttpGet("/testMultipleQueries")]
196-
public async Task<IActionResult> TestMultipleQueries(CancellationToken token)
227+
public async Task<IActionResult> TestMultipleQueries(CancellationToken cancellationToken)
197228
{
198229
//notify framework we want the total number of products
199230
var redProductsCountPromise = _dbContext.WrapQuery(
@@ -227,14 +258,14 @@ public async Task<IActionResult> TestMultipleQueries(CancellationToken token)
227258
.Deferred();
228259

229260
//execute all deferred queries. If the Database doesn't support futures (Ex: Oracle) the query will get executed when Value() is called
230-
var customersWithEmptyCart = await customersWithEmptyCartPromise.ListAsync(token);
261+
var customersWithEmptyCart = await customersWithEmptyCartPromise.ListAsync(cancellationToken);
231262
//we can unproxy the object at any time
232263
customersWithEmptyCart = _dbContext.Unproxy(customersWithEmptyCart);
233264

234265
// List/ListAsync shouldn't hit the Database unless it doesn't support futures. Ex: Oracle
235-
var expensiveProducts = await expensiveProductsPromise.ListAsync(token);
236-
var lastActiveCustomer = await lastActiveCustomerPromise.ValueAsync(token);
237-
var numberOfRedProducts = await redProductsCountPromise.ValueAsync(token);
266+
var expensiveProducts = await expensiveProductsPromise.ListAsync(cancellationToken);
267+
var lastActiveCustomer = await lastActiveCustomerPromise.ValueAsync(cancellationToken);
268+
var numberOfRedProducts = await redProductsCountPromise.ValueAsync(cancellationToken);
238269
return Ok(new
239270
{
240271
NumberOfRedProducts = numberOfRedProducts,
@@ -244,17 +275,58 @@ public async Task<IActionResult> TestMultipleQueries(CancellationToken token)
244275
});
245276
}
246277

278+
/// <summary>
279+
/// Execute your own SQL script using: ExecuteListAsync, ExecuteScalarAsync, ExecuteNonQueryAsync
280+
/// </summary>
281+
/// <param name="cancellationToken"></param>
282+
/// <param name="customerId"></param>
283+
/// <returns></returns>
247284
[HttpGet("/testSqlQuery")]
248-
public async Task<IActionResult> TestSqlQuery(CancellationToken token, int customerId = 11)
285+
public async Task<IActionResult> TestSqlQuery(CancellationToken cancellationToken, int customerId = 11)
249286
{
250287
var sqlQuery = @"select Id as CustomerId,
251288
Concat(FirstName,' ',LastName) as FullName,
252289
BirthDate
253290
from ""Customer""
254291
where Id= :customerId";
255-
var customResult = await _dbContext.ExecuteScalarAsync<SqlQueryCustomResult>(sqlQuery, new { customerId }, token);
292+
var customResult = await _dbContext.ExecuteScalarAsync<SqlQueryCustomResult>(sqlQuery, new { customerId }, cancellationToken);
256293
return Ok(customResult);
257294
}
295+
296+
297+
/// <summary>
298+
/// Execute a procedure or multiple queries which return multiple results.
299+
/// You'll need to cast/select the right collection.
300+
/// </summary>
301+
/// <param name="cancellationToken"></param>
302+
/// <param name="customerId"></param>
303+
/// <returns></returns>
304+
[HttpGet("/testDataSetQuery")]
305+
public async Task<IActionResult> TestDataSetQuery(CancellationToken cancellationToken, int customerId = 11)
306+
{
307+
var sqlQuery = @"select Id as CustomerId,
308+
Concat(FirstName,' ',LastName) as FullName,
309+
BirthDate
310+
from ""Customer""
311+
where Id= :customerId;
312+
select count(*) Count from ""Customer"";";
313+
var customResult = await _dbContext.ExecuteMultipleQueriesAsync(sqlQuery, //query
314+
new { customerId }, //parameters: property name must be the same as the parameter
315+
cancellationToken,
316+
typeof(SqlQueryCustomResult), //first result type
317+
typeof(long));//second result type
318+
319+
//The results are returned in order in it's own collection.
320+
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
322+
323+
return Ok(new
324+
{
325+
Customer = customer,
326+
CustomerCount = customerCount
327+
});
328+
}
329+
258330
class SqlQueryCustomResult
259331
{
260332
public SqlQueryCustomResult() { }

NHUnitExample/Startup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public void ConfigureServices(IServiceCollection services)
6868
private ISessionFactory CreateSessionFactory()
6969
{
7070
var connectionString = Configuration.GetSection("NhibernateConfig").Get<NhibernateConfig>().ConnectionString;
71+
//var dbCfg = OracleManagedDataClientConfiguration.Oracle10.Dialect<Oracle12cDialect>().ConnectionString(db => db.Is(connectionString));
7172
var dbCfg = PostgreSQLConfiguration.Standard.Dialect<PostgreSQL83Dialect>().ConnectionString(db => db.Is(connectionString));
7273
return Fluently.Configure()
7374
.Database(dbCfg)

README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ MIT: Enjoy, share, contribute to the project.
99

1010
## Where can I get it?
1111

12-
Install using the [NuGet package](http://nuget.org):
12+
Install using the [NuGet package](https://www.nuget.org/packages/NHUnit):
1313

1414
```
1515
dotnet add package NHUnit
@@ -26,17 +26,47 @@ The example project should get you going in no time with NHUnit (and NHibernate)
2626

2727

2828
## How do I use it?
29-
* Check FluentNHibernateExample project
29+
** Check [FluentNHibernateExample](https://github.com/CSharpBender/NHUnit/tree/master/NHUnitExample/) project **
3030
- In `appsetting.json` update `NhibernateConfig.ConnectionString`
3131
- In `Startup.cs` update the `CreateSessionFactory` method with your desired Database type. It uses PostgreSql.
3232
- Depending on the database type you might need to update the classes from the `Mappings` folder
3333
- Start the project and test the endpoints in the displayed order. The database will be recreated every time the projects starts unless you comment `BuildSchema` from `Startup.cs`
34+
- You should replace the NHUnit project reference with the [package](https://www.nuget.org/packages/NHUnit)
3435

3536
You can either inject IUnitOfWork and IRepository<T>'s in your controller or create your own implementation similar to EntityFramework. The example contains the custom implementation.
3637

3738

3839
# Documentation
3940

41+
## Create your Unit of work
42+
Register [NHibernate.ISessionFactory](https://github.com/CSharpBender/NHUnit/blob/master/NHUnitExample/Startup.cs#L63) into your dependency injection and define the interface for your Database.
43+
44+
```csharp
45+
public interface IDbContext : IUnitOfWork
46+
{
47+
IRepository<Customer> Customers { get; }
48+
IRepository<Product> Products { get; }
49+
}
50+
51+
public class DbContext : UnitOfWork, IDbContext
52+
{
53+
public DbContext(ISessionFactory sessionFactory) : base(sessionFactory, true) { }
54+
55+
public IRepository<Customer> Customers { get; set; }
56+
public IRepository<Product> Products { get; set; }
57+
}
58+
```
59+
60+
The framework automatically initializes all your `IRepository` properties when the second constructor parameter is true, otherwise you will need to do this yourself:
61+
62+
```csharp
63+
public DbContext(ISessionFactory sessionFactory) : base(sessionFactory) {
64+
Customers = new Repository<Customer>();
65+
Products = new Repository<Product>();
66+
}
67+
```
68+
69+
4070
## Eager loading
4171
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.
4272
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.
@@ -102,6 +132,7 @@ In some rare cases you need to execute your own queries and NHUnit provides this
102132
- ExecuteListAsync
103133
- ExecuteScalarAsync
104134
- ExecuteNonQueryAsync
135+
- ExecuteMultipleQueries
105136

106137
```csharp
107138
var sqlQuery = @"select Id as CustomerId,

0 commit comments

Comments
 (0)