An Object Relational Mapper (ORM) for people who like stored procedures.
Prius is an extremely efficient way of executing queries or stored procedures in your database, and mapping the results onto objects. Prius provides a lot of convenience and the advanced code generation techniques is uses often produces higher performance results than hand coding ADO.Net.
Apart from being convenient and fast, Prius also adds enterprise level features like measuring database performance and switching to alternate connections, and throttling database access to allow the database to recover from performance bottlenecks, and master/slave replication scenarios.
Note that in Prius 1 all of the database drivers were included in the main NuGet package. This had the advantage that it's one package to install which is simple, but has the disadvantage that your application has dependencies on MySql and PostgreSQL even if you don't use these databases.
In Prius 2 I am planning to expand to more database servers, starting with SqlLite. The model of including all database drivers in the one assembly works less well as I add more database drivers, to I split these out into separate NuGet packages.
If you are updating from Prius 1 to Prius 2, you will need to add the NuGet packages for the databases that you use or you will get a runtime error when the first database connection is attempted.
If the Prius configuration file there is type
property for each database
.
The table below lists the values of this type
property and the corresponding
NuGet package that you need to install.
Database type |
NuGet package name |
---|---|
SqlServer | Prius.SqlServer |
MySql | Prius.MySql |
PostgreSql | Prius.PostgreSQL |
SQLite | Prius.SQLite (1) |
(1) See readme file in the Prius.SQLite folder.
This is an example of calling a stored procedure with some parameters and returning the results as a collection of objects:
By default database columns are mapped to properties with the same name:
using Prius.Contracts.Interfaces;
internal class Profile
{
long ProfileId { get; set; }
string UserName { get; set; }
}
internal class MyClass
{
private readonly IContextFactory _contextFactory;
private readonly ICommandFactory _commandFactory;
public MyClass(IContextFactory contextFactory, ICommandFactory commandFactory)
{
_contextFactory = contextFactory;
_commandFactory = commandFactory;
}
public Profile GetProfile(long profileId)
{
try
{
using (var context = _contextFactory.Create("MyDatabase"))
{
using (var command = _commandFactory.CreateStoredProcedure("getProfileById"))
{
command.AddParameter("id", profileId);
using(var results = context.ExecuteEnumerable<Profile>(command))
return results.FirstOrDefault();
}
}
}
catch (Exception ex)
{
throw new Exception("Failed to retrieve profile id " +
profileId + " from the database", ex);
}
}
}
You can also use different names in the database and in your C#.
/// <summary>
/// An example of a data contract with declarative mapping to DB columns
/// </summary>
internal class User
{
// Maps the Name property onto the 'userName' column in the database, if
// the database contains null the Name property will ne Null
[Mapping("userName")]
public string Name { get; set; }
// Maps the Age property onto the 'userAge' column in the database.
// If the database contains NULL then the Value property will be set to -1
[Mapping("userAge", -1)]
public int Age { get; set; }
// Maps the Description property onto the 'descr' column in the
// database. If the database contains NULL then the Description property
// will be set to an empty string
[Mapping("descr", "")]
public string Description { get; set; }
}
Note that column names are case insensitive.
Note that if you don't add any
[Mapping]
attributes to your data contract then Prius will map all public properties into database columns with the same names. If you add one or more[Mapping]
attributes then properties without[Mapping]
attributes will not be mapped and will not have their values set by Prius (unless you also implementIDataContract
- see below).Note that you can add multiple
[Mapping]
attributes to one property, in which case database columns with any of these names will map to this property. This is useful when different stored procedures return the same column with different names because of column aliasing.
Note that Prius also allows you to add declarative field mappings to interfaces
instead of concrete types. Note that when you do this the IFactory
that you
provide to Prius must be capable of constructing objects from interface types.
To use the mappings defined for an interface, pass the interface type when
calling the ExecuteEnumerable()
method.
You might want to use interface data contracts instead of concrete class types because:
You want to have more than one way to map to a class onto the database. You
can make the class implement multiple interfaces and decorate each interface
with different [Mapping]
attributes.
You want to return objects whose type is imported from a DLL that you do not
have source code for and therefore can not have [Mapping]
attributes added
to it. In this case you can inherit from the imported type and
add an interface to the derrived class decorated with [Mapping]
attributes.
This is more efficient than defining a new class and copying the data using
something like AutoMapper
.
You just prefer using interfaces. There is nothing wrong with concrete data contracts provided they do not define any methods, but some developers prefer to define everything in terms of interfaces.
For greater flexibility you can also implement the IDataContract
interface, in
this case any declarative mappings will be applied first, then your IDataContract
implementation will execute, and can modify the mappings.
internal enum Enum1 { Value1, Value2, Value3 }
internal class SampleDataContract: IDataContract<SampleDataContract>
{
public string Name { get; set; }
public int Value { get; set; }
public string Description { get; set; }
public string Title { get; set; }
public Enum1 MyEnum { get; set; }
public void AddMappings(ITypeDefinition<SampleDataContract> typeDefinition, string dataSetName)
{
typeDefinition.AddField("name", c => c.Name, string.Empty);
typeDefinition.AddField("value", c => c.Value, -1);
typeDefinition.AddField("descr", (c, v) => c.Description = v.ToLower(), string.Empty);
typeDefinition.AddField("enum", c => c.MyEnum, Enum1.Value1);
}
public void SetCalculated(IDataReader dataReader, string dataSetName)
{
Title = Name + "=" + Value;
}
}
For ultimate flexibility you can also implement the IDataContract
interface in a way that
creates different column mappings for different stored procedures. This is useful only
when your database returns different data in the same column names depending on
which stored procedure you call, for example if some stored procedures return age as a string
and others return age as an integer even though they both return essentially the same data
(this would be pretty messed up I know, but I have had to work with legacy databases that have
these kinds of problems).
internal enum Enum1 { Value1, Value2, Value3 }
internal class SampleDataContract: IDataContract<SampleDataContract>
{
public string Name { get; set; }
public int Value { get; set; }
public string Description { get; set; }
public string Title { get; set; }
public Enum1 MyEnum { get; set; }
public void AddMappings(ITypeDefinition<SampleDataContract> typeDefinition, string dataSetName)
{
if (string.Equals(dataSetName, "someWeirdSproc", StringComparison.OrdinalIgnoreCase))
{
typeDefinition.AddField("n", c => c.Name, string.Empty);
typeDefinition.AddField("v", c => c.Value, -1);
typeDefinition.AddField("d", (c, v) => c.Description = v.ToLower(), string.Empty);
typeDefinition.AddField("e", c => c.MyEnum, Enum1.Value1);
}
else
{
typeDefinition.AddField("name", c => c.Name, string.Empty);
typeDefinition.AddField("value", c => c.Value, -1);
typeDefinition.AddField("descr", (c, v) => c.Description = v.ToLower(), string.Empty);
typeDefinition.AddField("enum", c => c.MyEnum, Enum1.Value1);
}
}
public void SetCalculated(IDataReader dataReader, string dataSetName)
{
Title = Name + "=" + Value;
}
}
Prius is all about convenience, programmer productivity and runtime performence. I tried to make it as easy as possible to use and very easy to understand without any compromise to runtime performance.
Feature | Prius | ADO.Net | nHibernate | EF |
---|---|---|---|---|
Supports SQL Server | Yes | Yes | Yes | Yes |
Supports MySQL | Yes | Yes | Yes | Yes |
Supports Postgsql | Yes | Yes | Yes | Yes |
Easy to add support for other databases | Yes | No | No | No |
Provides load balancing | Yes | No | No | No |
Provides fail over | Yes | No | No | No |
Monitors database health | Yes | No | No | No |
Can map results from queries onto objects | Yes | No | Yes | Yes |
Has built-in lazy loading | No | No | Yes | Yes |
Can execute parameterized ad-hoc queries | Yes | Yes | Yes | Yes |
Can execute stored procedures | Yes | Yes | Yes | Yes |
Can execute asynchronous requests | Yes | Yes | Yes | Yes |
Can generate efficient SQL from LINQ | No | No | Yes | Yes |
Can use IoC container to construct data models | Yes | No | No | No |
Can generate database from data model | No | No | Yes | Yes |
Can generate data model from the database | No | No | Yes | Yes |
Can fill any class with query results | Yes | No | No | No |
Can use data model from external library | Yes | No | No | No |
Can fill objects using results from multiple stored procedure calls | Yes | No | No | No |
Can easily handle stored procedures that return multiple data sets | Yes | No | No | No |
Separates code and configuration for things like stored procedure timeout | Yes | No | No | No |
Routes requests to master or read-only replica without code changes | Yes | No | No | No |
EF refers to the Microsoft Entity Framework.
In the Visual Studio solution for Prius there are projects that you can run to test the runtime performance of Prius on your hardware, and compare Prius to other alternatives. The results of running these tests on my hardware are summarized in the following table:
Test | Iterations | Baseline | Prius | EF | ADO.Net |
---|---|---|---|---|---|
Do nothing | 1 | 51us | 32us | 39us | |
Do nothing | 1000 | 6ns | 5ns | 6ns | |
Retrieve one customer | 1 | 1.8ms | 93ms | 78ms | |
Retrieve one customer | 100 | 0.9us | 0.22ms | 0.25ms | |
One customer with orders | 1 | 2.2ms | 14ms | 7.2ms | |
One customer with orders | 100 | 4.8us | 0.76ms | 0.22ms | |
Selected customers | 1 | 12ms | 19ms | 11.5ms | |
Selected customers | 100 | 1.7ms | 6.8ms | 3.3ms | |
Selected customers lazy load orders | 1 | 2.9ms | 8.1ms | 8.6ms | |
Selected customers lazy load orders | 100 | 1.7ms | 6.7ms | 10.4ms | |
Selected customers with orders | 1 | 3.4ms | 134ms | 794ms | |
Selected customers with orders | 100 | 2.5ms | 104ms | 589ms | |
All customers | 1 | 1.1ms | 6.1ms | 3.2ms | |
All customers | 100 | 1.2ms | 9.6ms | 2.5ms | |
All customers with orders | 1 | 3ms | 99ms | 501ms | |
All customers with orders | 100 | 1.2ms | 101ms | 513ms |
Each test was run once and the time taken recorded in this table, then the test was run again multiple times and the average time recorded in this table. The tests were done like that because there is often a startup cost (for example Prius uses reflection to build a map one time only, so the very first usage takes longer). Most real-world applications will call the database many times for the same type of data, so the values in this table for multiple iterations are the most useful ones.
The 'Do nothing' test just tests an empty statement and is included so you can see the overhead of the testing framework itself. The 'Baseline' project implements the data access layer by constructing objects and filling them with random data. This enables you to get an idea of how long that part of the operation takes compared to retrieving from the DB and constructing/filling the objects.
Prius uses ADO.Net to connect to the database, and adds an object mapping layer on top. You can see the cost of the ORM and other Prius features (such as load balancing and failover) by comparing Prius with ADO.Net.
I ran these tests on a 4GB Microsoft Surface Pro 3 with i5 processor and an external 240GB SSD connected via USB 3.0. The software was Windows 10, Visual Studio 2013 and SQL Server 2014 Express.
Prius.Performance.Shared contains the actual tests. This makes sure there is a level playing field between the technologies being tested. The test defines a customer with orders and a data access layer that can retrieve customers and lazily load their orders. The data access layer is implemented in each technology and the exact same tests are run against each implementation.
Prius.Performance.Dummy contains a data access layer that does no data access. This can be used as a baseline for comparing the other technologies.
Prius.Performance.Prius contains a data access layer implementation that uses Prius so that we can measure how fast Prius is. This project is also a good example of a fairly minimal application that has Prius integrated into it. It also demonstrates a number of ways to work with Prius, for example stored procedures that return multiple result sets, executing multiple queries in parallel, data contracts with injected dependencies etc.
Prius.Performance.EntityFramework contains a data access layer implementation that uses the Microsofts Entity Framework so that we can measure how fast it is for the same set of tests.
Prius.Performance.Ado contains a data access layer implementation that uses the Microsofts ADO.Net Framework that we can measure how fast it is for the same set of tests.
Ioc.Modules
If you are already using the Ioc.Modules
NuGet package in your application then most
of the IoC configuration will happen automatically. You will need to implement two interfaces
in your application, and add them to an Ioc.Modules
package class.
The interfaces you need to implement are:
Interface | Description |
---|---|
IFactory |
Used to construct instances when you map database results onto classes. You only have to implement a couple of very simple methods. |
IErrorReporter |
Used to report errors. This interface also defines a couple of very straightforward methods. |
Your Package.cs
file should look something like this:
using System.Collections.Generic;
using Ioc.Modules;
using Prius.Contracts.Interfaces.External;
namespace MyApp
{
[Package]
public class Package: IPackage
{
public string Name { get { return "My application"; } }
public IList<IocRegistration> IocRegistrations
{
get
{
return new List<IocRegistration>
{
new IocRegistration().Init<IFactory, PriusFactory>(),
new IocRegistration().Init<IErrorReporter, PriusErrorReporter>(),
};
}
}
}
}
Ioc.Modules
The recommended method if integration is to use an IoC container - but you do not have to. If you are using IoC, then you need to map these Prius interfaces onto classes that are provided by Prius. As always with IoC you can also substitute the Prius implementation for your own implementation to customize the behaviour.
Interface | Class |
---|---|
ICommandFactory |
CommandFactory |
IConnectionFactory |
ConnectionFactory |
IContextFactory |
ContextFactory |
IDataEnumeratorFactory |
DataEnumeratorFactory |
IDataReaderFactory |
DataReaderFactory |
IMapper |
Mapper |
IParameterFactory |
ParameterFactory |
IRepositoryFactory |
RepositoryFactory |
IEnumerableDataFactory |
EnumerableDataFactory |
IAsyncEnumerableFactory |
AsyncEnumerableFactory |
In addition you must write classes in your application that implement the interfaces described
above in the section about using Ioc.Modules
It is recommended that you implement IFactory
using your IoC container so that you can
map the results from the database onto objects that have dependencies. If you are mapping
only data contracts with default public constructors, then you can write a simpler and faster
version of IFactory
that calls the default public constructor instead.
Note that Prius uses Urchin for its configuration, so Urchin must also be registered in your IoC container. See Urchin documentation for how to do this. Note that if you are using
Ioc.Modules
then Urchin will be configured in your IoC container automatically.
In this version, Prius supports Microsoft SQL Server, MySQL and Postgresql. This is an open source project, and adding support for another database only means adding 3 new source files, so please give back to the community if your database is not one of these.
One of the goals of Prius is to make your code identical no matter which database you are using. The type of database is simply a configuration option. This makes it especially useful for reusable NuGet packages, where users will prefer to stick with their existing database technology.
Prius uses Urchin for it's configuration. Urchin allows all application configuration to be stored in a central server, and also allows configuration for different applications, machines and environments to be specified using rules to avoid duplication of configuration.
Another feature of Urchin that Prius makes use of is the configuration change notification. Whenever the configuration of an application that uses Prius is changed, Prius will start using the new configuration within a few seconds without restarting the application. This will allow you for example to fail all applications over to a backup database server by changing a rule on the Urchin configuration service.
This is a sample Urchin configuration for Prius:
{
"prius": {
"databases": [
{
"name": "db1",
"type": "SqlServer",
"connectionString": "",
"procedures": [
{"name": "Sproc1", "timeout": 3},
{"name": "Sproc2", "timeout": 7}
]
},
{
"name": "db2",
"type": "MySQL",
"role": "master",
"connectionString": "",
"procedures": [
{"name": "Sproc1", "timeout": 6},
{"name": "Sproc2", "timeout": 15}
]
},
{
"name": "db3",
"type": "MySQL",
"role": "slave",
"connectionString:"",
"procedures": [
{"name": "Sproc1", "timeout": 6},
{"name": "Sproc2", "timeout": 15}
]
}
],
"fallbackPolicies": [
{"name": "primary", "allowedFailurePercent": 20, "backOffTime": "00:01:00"},
{"name": "backup", "allowedFailurePercent": 100}
],
"repositories": [
{
"name": "users",
"clusters": [
{"sequence": 1, "databases": ["db1"], "fallbackPolicy": "primary"},
{"sequence": 2, "databases": ["db2", "db3"], "fallbackPolicy": "backup"}
],
"procedures": [
{"name": "Sproc1", "roles": ["master"]},
{"name": "Sproc2", "roles": ["master", "slave"]}
]
}
]
}
}
What this configuration example does is:
Defines database connections, one to SqlServer database and two MySQL databases. I left the connection strings blank to keep the example simple.
Defines a 'primary' fallback policy that will fall over to the backup server for 1 minute if more than 20% of database requests error or timeout.
Defines a 'backup' fallback policy that will not fail over even when the error rate is 100%.
Defines a 'users' repository that uses Microsoft SQL server initially, but fails over to a pair of MySQL databases if SQL Server is slow or unavailable. The MySQL databases have master/slave replication set up where stored procedures that modify the data must be executed against the master database.
Note that because the code you write in your application is identical for all databases, it is possible for Prius to fall back from SQL Server to MySQL.
Note that for this to work, SQL Server and MySQL must contain all the same stored procedures.
Note that when you call the
Create()
method ofIContextFactory
, it is the name of the repository that you pass. In this example_contextFactory.Create("users");
Note that you can define timeout values for each stored procedure on each server. Any stored procedures that you dont define timeouts for will default to 5 seconds.
Note that you can also pass a timeout value in the code that calls the stored procedure, but this is generally less maintainable than the configuration based approach. Remember that Prius uses the Urchin rules based configuration management system that can define environment specific rules.
Note that stored procedures can be configured to run on servers with specific roles. Stored procedures that are not configured in this way will be executed against any available server.
If you want to test that your requests are going to the right database you can enable a tracing mechanism. This is only recommended in a development or test environment. It is not recommended to turn on tracing in a busy production environment.
To enable tracing you must write a class that implements ITraceWriterFactory
then pass an instance of it to the EnableTracing
method of IRepositoryFactory
singelton before opening any database connections. The UsersTestApp
project is
an example of how to do this (see Program.cs
, TraceWriter.cs
and TraceWriterFactory.cs
).
If you want to get some insight into how many connections your application is making to the database, the ration of sucesfull to failed commands, or the average time it takes to execute a particular stored procedure then you can turn on analytic reporting. This will add some CPU cycles to all database accesses so make sure your servers are not close to theor limit before enabling this option.
To enable analytics you must write a class that implements IAnalyticRecorderFactory
then pass an instance of it to the EnableAnalyticReporting
method of IRepositoryFactory
singelton before opening any database connections. The UsersTestApp
project is
an example of how to do this (see Program.cs
and AnalyticRecorderFactory.cs
).
Be aware that any code you write to gather analytics will be executed in the context of requests to the database and this will slow down all database operations. You should be mindful of writing scaleable and thread-safe code in your implementation.
using Prius.Contracts.Interfaces;
public class DataAccessLayer : IDataAccessLayer
{
private readonly IContextFactory _contextFactory;
private readonly ICommandFactory _commandFactory;
private readonly IMapper _mapper;
private readonly IDataEnumeratorFactory _dataEnumeratorFactory;
private readonly IEnumerableDataFactory _enumerableDataFactory;
public DataAccessLayer(
IContextFactory contextFactory,
ICommandFactory commandFactory,
IMapper mapper,
IDataEnumeratorFactory dataEnumeratorFactory,
IEnumerableDataFactory enumerableDataFactory)
{
_contextFactory = contextFactory;
_commandFactory = commandFactory;
_mapper = mapper;
_dataEnumeratorFactory = dataEnumeratorFactory;
_enumerableDataFactory = enumerableDataFactory;
}
}
Note that you always need
IContextFactory
andICommandFactory
. The other interfaces are only needed for some more advanced techniques.
public IList<ICustomer> GetCustomers()
{
using (var context = _contextFactory.Create("MyData"))
{
using (var command = _commandFactory.CreateStoredProcedure("sp_GetAllCustomers"))
{
using (var data = context.ExecuteEnumerable<Customer>(command))
return data.ToList();
}
}
}
public ICustomer GetCustomer(int customerId)
{
using (var context = _contextFactory.Create("MyData"))
{
using (var command = _commandFactory.CreateStoredProcedure("sp_GetCustomer"))
{
command.AddParameter("CustomerID", customerId);
using (var data = context.ExecuteEnumerable<Customer>(command))
return data.FirstOrDefault();
}
}
}
public ICustomer DeleteCustomer(int customerId)
{
using (var context = _contextFactory.Create("MyData"))
{
using (var command = _commandFactory.CreateStoredProcedure("sp_DeleteCustomer"))
{
command.AddParameter("CustomerID", customerId);
context.ExecuteNonQuery(command));
}
}
}
public bool InsertCustomer(ICustomer customer)
{
using (var context = _contextFactory.Create("MyData"))
{
using (var command = _commandFactory.CreateStoredProcedure("sp_InsertCustomer"))
{
var idParam = command.AddParameter("CustomerID", SqlDbType.Int);
var rowsAffected = context.ExecuteNonQuery(command));
if (rowsAffected != 1)
return false;
customer.CustomerId = (int)idParam.Value;
return true;
}
}
}
Note that the context.ExecuteEnumerable
method is a shorthand syntax that works for the most common use case of a
single data set. To work with multiple sets of data you have to use a slightly more verbose syntax, but this results
in the same internal operation.
In this example the stored procedure returns a single customer record in the first data set and a list of the customer's orders in the second data set. This example therefore demonstrates two different techniques
public ICustomer GetCustomerAndOrders(int customerId)
{
using (var command = _commandFactory.CreateStoredProcedure("dbo.sp_GetCustomerAndOrders"))
{
command.AddParameter("CustomerID", customerId);
using (var context = _contextFactory.Create("MyData"))
{
using (var reader = context.ExecuteReader(command))
{
if (reader.Read())
{
var customer = _mapper.Map<Customer>(reader);
if (reader.NextResult())
{
using (var orderEnumerator = _dataEnumeratorFactory.Create<Order>(reader))
customer.Orders = orderEnumerator.Cast<IOrder>().ToList();
}
return customer;
}
}
}
}
}
Note that the database context can only have one open data reader at a time, so you will need multiple context objects to execute multiple stored procedures concurrently. You could nest using statements, but this can get very deep if you have many concurrent requests. In this example I used a try...finally instead.
Note that if you are using .Net 4.5 or higher then you can use the async...await mechanism for this instead.
public ICustomer GetCustomersAndOrders(int customerId)
{
var customerContext = _contextFactory.Create("MyData");
var customerCommand = _commandFactory.CreateStoredProcedure("dbo.sp_GetCustomer");
customersCommand.AddParameter("CustomerID", customerId);
var ordersContext = _contextFactory.Create("MyData");
var ordersCommand = _commandFactory.CreateStoredProcedure("dbo.sp_GetCustomerOrders");
ordersCommand.AddParameter("CustomerID", customerId);
try
{
var customerResult = customerContext.BeginExecuteEnumerable(customerCommand);
var ordersResult = ordersContext.BeginExecuteEnumerable(ordersCommand);
WaitHandle.WaitAll(new[] { customerResult.AsyncWaitHandle, ordersResult.AsyncWaitHandle });
Customer customer;
using (var customerRecords = customerContext.EndExecuteEnumerable<Customer>(customerResult))
customer = customerRecords.FirstOrDefault();
if (customer == null)
return null;
using (var orderRecords = ordersContext.EndExecuteEnumerable<Order>(ordersResult))
customer.Orders = orderRecords.ToList();
}
finally
{
customerContext.Dispose();
customerCommand.Dispose();
ordersContext.Dispose();
ordersCommand.Dispose();
}
}
This example assumes that news articles have a very large 'content' column that isn't required most of the time,
so the sp_GetNewsArticle
stored procedure does not return the 'content' column. There is a separate stored
procedure that only returns the 'content' column.
Note that in this example both stored procedures are in the same database, but you could have the second stored procedure in a different database, and this database could even use a different database technology.
public class NewsArticle
{
public long NewsArticleId { get; set; }
public DateTime PublishedDate { get; set; }
public string Source { get; set; }
public string Headline { get; set; }
public string Content { get; set; }
}
public NewsArticle GetNewsArticle(long newsArticleId, bool includeContent = false)
{
using (var context = _contextFactory.Create("MyData"))
{
NewsArticle newsArticle;
using (var command = _commandFactory.CreateStoredProcedure("sp_GetNewsArticle"))
{
command.AddParameter("NewsArticleID", newsArticleId);
using (var data = context.ExecuteEnumerable<NewsArticle>(command))
newsArticle = data.FirstOrDefault();
}
if (includeContent && newsArticle != null)
{
using (var command = _commandFactory.CreateStoredProcedure("sp_GetNewsArticleContent"))
{
command.AddParameter("NewsArticleID", newsArticleId);
using (var reader = context.ExecuteReader(command))
{
if (reader.Read())
_mapper.Fill(newsArticle, reader);
}
}
}
return newsArticle;
}
}
public interface ICustomer
{
[Mapping("fld_CustomerID")]
long Id { get; set; }
[Mapping("fld_CustomerName")]
string Name { get; set; }
}
public IList<ICustomer> GetCustomers()
{
using (var context = _contextFactory.Create("MyData"))
{
using (var command = _commandFactory.CreateStoredProcedure("sp_GetAllCustomers"))
{
using (var data = context.ExecuteEnumerable<ICustomer>(command))
return data.ToList();
}
}
}
To use this mechanism you need to have a dependency on the IEnumerableDataFactory
interface
then call its Create
method to wrap the context and the data enumerator in a new
enumerable collection that is also disposable. Disposing of the result disposes of the
data enumerator and the context - closing the connection.
public IDisposableEnumerable<ICustomer> GetCustomers()
{
var context = _contextFactory.Create("MyData"))
try
{
using (var command = _commandFactory.CreateStoredProcedure("sp_GetAllCustomers"))
{
var data = context.ExecuteEnumerable<Customer>(command))
if (data == null) return null;
var result = _enumerableDataFactory.Create(context, data);
context = null;
return result;
}
}
finally
{
if (context != null) context.Dispose();
}
}