So NHibernate has been released with .NET Core support! NHibernate Announcement
Which means in theory we can use that as an ORM and combined with the .NET Core support from Oracle.ManagedDataAccess we can use it with Oracle Databases!
So in this example, we will use the previous Linq2Db examples and move those over to NHibernate! We will still maintain the Sqlite tests we had before.
This example references and builds off stuff contained in a previous example. When I am referring to Linq2Db I am referring to code in that example.
So for our Northwind domain when using NHibernate we are going to want three basic sections:
namespace Northwind.Models
{
public class Product
{
//PRODUCTS
public int Id { get; set; } //PRODUCT_ID
public string ProductName { get; set; } //PRODUCT_NAME
public int SupplierId { get; set; } //SUPPLIER_ID
public int CategoryId { get; set; } //CATEGORY_ID
public string QuantityPerUnit { get; set; } //QUANTITY_PER_UNIT
public double UnitPrice { get; set; } //UNIT_PRICE
public int UnitsInStock { get; set; } //UNITS_IN_STOCK
public int UnitsOnOrder { get; set; } //UNITS_ON_ORDER
public int ReorderLevel { get; set; } //REORDER_LEVEL
public string Discontinued { get; set; } //DISCONTINUED
}
}
And then...
using NHibernate.Mapping.ByCode.Conformist;
using Northwind.Models;
namespace Northwind.Mappings
{
//NHIBERNATE MAPPING
internal class ProductsMapping : ClassMapping<Product>
{
public ProductsMapping()
{
Schema("NORTHWIND");
Table("PRODUCTS");
Lazy(false);
Id(prop => prop.Id, map =>
{
map.Column("PRODUCT_ID");
//map.Generator(Generators.Sequence, gmap => gmap.Params(new {sequence = "PRODUCT_ID_SEQ"}));
});
Property(prop => prop.ProductName, map => map.Column("PRODUCT_NAME"));
Property(prop => prop.SupplierId, map => map.Column("SUPPLIER_ID"));
Property(prop => prop.CategoryId, map => map.Column("CATEGORY_ID"));
Property(prop => prop.QuantityPerUnit, map => map.Column("QUANTITY_PER_UNIT"));
Property(prop => prop.UnitPrice, map => map.Column("UNIT_PRICE"));
Property(prop => prop.UnitsInStock, map => map.Column("UNITS_IN_STOCK"));
Property(prop => prop.UnitsOnOrder, map => map.Column("UNITS_ON_ORDER"));
Property(prop => prop.ReorderLevel, map => map.Column("REORDER_LEVEL"));
Property(prop => prop.Discontinued, map => map.Column("DISCONTINUED"));
}
}
}
Finally for the Northwind project we need a static Factory to create the NHibernate SessionFactory! Thus a file called NhFactory.cs:
There are some things I want to point out here:
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using NHibernate;
using NHibernate.Cache;
using NHibernate.Cfg;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Connection;
using NHibernate.Dialect;
using NHibernate.Driver;
using NHibernate.Mapping.ByCode;
namespace Northwind
{
public static class NhFactory
{
public static HbmMapping[] HbmMappings { get; private set; }
public static ISessionFactory CreateNhSessionFactory<TDialect, TDriver>(string connectionString, Assembly[] assemblies = null, bool showSql = true)
where TDialect : Dialect
where TDriver : IDriver
{
if (assemblies == null)
{
assemblies = new[] { Assembly.GetExecutingAssembly() };
}
var nhConfiguration = new Configuration();
nhConfiguration.Cache(properties => properties.Provider<HashtableCacheProvider>());
nhConfiguration.DataBaseIntegration(dbi =>
{
dbi.Dialect<TDialect>();
dbi.Driver<TDriver>();
dbi.ConnectionProvider<DriverConnectionProvider>();
dbi.IsolationLevel = IsolationLevel.ReadCommitted;
dbi.ConnectionString = connectionString;
dbi.Timeout = 60;
dbi.LogFormattedSql = true;
dbi.LogSqlInConsole = false;
});
if (showSql)
{
nhConfiguration.Properties["show_sql"] = "true";
}
if (HbmMappings == null || !HbmMappings.Any())
{
RegisterMappings(assemblies);
}
if (HbmMappings != null)
{
HbmMappings.ToList().ForEach(nhConfiguration.AddMapping);
var assembly = Assembly.GetExecutingAssembly();
nhConfiguration.AddAssembly(assembly);
var sessionFactory = nhConfiguration.BuildSessionFactory();
return sessionFactory;
}
throw new HibernateConfigException("Unable to find any mappings for NHibernate.");
}
public static void RegisterMappings(Assembly[] assemblies)
{
//Register things that are classes ending with Mapper and in the Mappings namespace
var mapper = new ModelMapper();
foreach (Type t in GetMappings(assemblies))
{
var method = typeof(ModelMapper).GetMethods().FirstOrDefault(
o => o.Name.Equals("AddMapping") &&
o.GetParameters().Length == 0 &&
o.ContainsGenericParameters
);
if (method == null)
{
throw new InvalidOperationException("Could not retrieve AddMapping method");
}
method
.MakeGenericMethod(t).Invoke(mapper, null);
}
var mappings = new List<ModelMapper>
{
mapper
};
var hibernateMappings = mappings.Select(map =>
{
var hbm = map.CompileMappingForAllExplicitlyAddedEntities();
hbm.autoimport = false;
return hbm;
}).ToArray();
HbmMappings = hibernateMappings;
}
public static IEnumerable<Type> GetMappings(IEnumerable<Assembly> assemblies)
{
var result = new List<Type>();
foreach (var assembly in assemblies)
{
result.AddRange(assembly.GetTypes()
.Where(o => o.Name.EndsWith("Mapping") && o.Namespace != null &&
o.Namespace.EndsWith("Mappings"))
.ToArray());
}
return result;
}
}
}
This factory is very generic, you can reuse it over and over again in future projects. Remember no dependencies on the Database Implementation.
In this example we have the models and mappings in the same project. It makes more sense for actual real life code to separate them. Have a Northwind.Models and a Northwind.DataAccess. That way you don't need to change your models if you decide to use something else for data access. Another benefit is if someone doesn't care about data access and only wants to use your models, less dependencies. Think... Separation of Concerns.
Our console application which displays a list of products will need to be changed. Those changes are going to look like this in Program.cs: (I added extra comments to explain what is happening)
using System;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Configuration;
using NHibernate.Dialect;
using NHibernate.Driver;
using Northwind;
using Northwind.Models;
namespace OdpNetCoreExample
{
class Program
{
public static IConfiguration Configuration { get; set; }
static void Main()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appSettings.json");
Configuration = builder.Build();
GetData();
}
static void GetData()
{
//Let's load a SessionFactory. This includes the schemas and mappings. And the database connection.
//Notice how we are passing in the dialect and driver? That means Northwind is Agnostic per
//the Database engine.
using (var sessionFactory =
NhFactory.CreateNhSessionFactory
And that is pretty much it for the Console application.
Our tests will get a bit simpler now. A plus is NHibernate respects our schema naming with Sqlite aka NORTHWIND.PRODUCTS (from Oracle) is NORTHWIND_PRODUCTS (in Sqlite).
Let's look at the NorthwindDbFixture.cs:
So what is happening here?
using System;
using System.IO;
using NHibernate;
using NHibernate.Dialect;
namespace Northwind.Tests
{
public class NorthwindDbFixture : IDisposable
{
public ISessionFactory SessionFactory { get; }
private readonly string[] _buildCommands;
public NorthwindDbFixture()
{
//DataSource=:memory: makes Sqlite use in memory
SessionFactory = NhFactory.CreateNhSessionFactory<SQLiteDialect, MicrosoftDataSqliteDriver> ("DataSource=:memory:");
using (var sr = new StreamReader("build_northwind.sql"))
{
var s = sr.ReadToEnd();
_buildCommands = s.Split(';');
}
}
public ISession OpenSession()
{
var session = SessionFactory.OpenSession();
foreach (var sql in _buildCommands)
{
using (var cmd = session.Connection.CreateCommand())
{
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
}
return session;
}
public void Dispose()
{
}
}
}
So NHibernate doesn't have a driver included in the nuget package for the Microsoft.Data.Sqlite.
We need to create one! Which is super easy. I took the code from NHibernate.Driver.CsharpSqliteDriver and changed it for Microsoft.Data.Sqlite.
MicrosoftDataSqliteDriver.cs
We are getting closer...
using NHibernate;
using NHibernate.Driver;
namespace Northwind.Tests
{
///
using System.Linq;
using Northwind.Models;
using Xunit;
using Xunit.Abstractions;
namespace Northwind.Tests
{
public class BasicTests : IClassFixture<NorthwindDbFixture>
{
//this is to display some output about the counts
private readonly ITestOutputHelper _output;
private readonly NorthwindDbFixture _fixture;
public BasicTests(ITestOutputHelper output, NorthwindDbFixture fixture)
{
_output = output;
_fixture = fixture;
}
[Fact]
public void CanGetProductsViaLinq()
{
using(var session = _fixture.OpenSession())
{
var query = session.Query<Product>().Where(o => o.Id > 25).OrderByDescending(o => o.ProductName)
.ToList();
_output.WriteLine("Products: {0}", query.Count);
Assert.True(query.Any());
}
}
[Fact]
public void CanGetCheapProductsViaLinq()
{
using (var session = _fixture.OpenSession())
{
var query = session.Query<Product>().Where(o => o.UnitPrice < 20).OrderByDescending(o => o.ProductName)
.ToList();
Assert.True(query.Any());
}
}
}
}
So not a lot of changes here. We are basically using the fixture directly instead of the Setup like in the Linq2Db example.
Our basic querying has changed from db.Product.Where(o => o.Id > 25) in Linq2Db to session.Query<Product>().Where(o => o.Id > 25) with NHibernate. The contents of the where remains the same.
I am not using transactions here, because this is read only. But maybe I should be?
At this point everything should build. If you run the console app you should see the products. If you run the tests those should pass.
So with the new .Net Core support provided by Oracle.ManagedDataAccess and the new NHibernate with .Net Core support you can now create platform portable solutions that can use an ORM and work with the Oracle Database. You should also be able to create tests using an in memory Sqlite Database to test your stuff.