I'm new to Castle, NHibernate and WCF.
I implemented the session management for my MVC application based on the following article because it seemd to be the most advanced implementation of all posts I've read so far :
http://nhibernate.info/blog/2011/03/02/effective-nhibernate-session-management-for-web-apps.html
The only problem I got was that this uses some Asp.net specific functionality that isn't available in my WCF service like (HttpContext.Current.Items).
I started to use WcfFacility
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.AddFacility<WcfFacility>().Register
(
Component.For<IRepository>().ImplementedBy(typeof(RepositoryBase<,>)),
Component.For<ITimeService>()
.ImplementedBy<myTimeMvc.Webservice.TimeService>()
.Named("myTimeMvc.Webservice.TimeService"));
container.Register(
Component.For<IServiceBehavior>()
.ImplementedBy<WcfSessionPerRequestBehavior>()
);
}
My Persistance configuration:
public class PersistenceInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Kernel.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<ISessionFactory>().UsingFactoryMethod(k => CreateNhSessionFactory()));
container.Register(Component.For<ISessionFactoryProvider>().AsFactory());
container.Register(Component.For<IEnumerable<ISessionFactory>>().UsingFactoryMethod(k => k.ResolveAll<ISessionFactory>()));
container.Register(Classes.FromAssembly(Assembly.GetAssembly(typeof(HdtRepository))).InSameNamespaceAs<HdtRepository>().WithService.DefaultInterfaces().LifestyleTransient());
}
/// <summary>
/// Creates NHibernate Session Factory.
/// </summary>
/// <returns>NHibernate Session Factory</returns>
private static ISessionFactory CreateNhSessionFactory()
{
var connStr = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
return Fluently.Configure()
.Database(
MsSqlConfiguration.MsSql2008
.UseOuterJoin()
.ConnectionString(x => x.FromConnectionStringWithKey("DefaultConnection"))
.ShowSql()
)
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<TimeRecord>())
.ExposeConfiguration(cfg =>
cfg.Properties[Environment.CurrentSessionContextClass] = typeof(LazySessionContext).AssemblyQualifiedName
)
.BuildSessionFactory();
}
}
Then i tried to solve the problem with "HttpContext.Current.Items" by adding a custom extension:
namespace MyTimeService.WcfExtension
{
///<summary>
/// This class incapsulates context information for a service instance
///</summary>
public class WcfInstanceContext : IExtension<InstanceContext>
{
private readonly IDictionary items;
private WcfInstanceContext()
{
items = new Hashtable();
}
///<summary>
/// <see cref="IDictionary"/> stored in current instance context.
///</summary>
public IDictionary Items
{
get { return items; }
}
///<summary>
/// Gets the current instance of <see cref="WcfInstanceContext"/>
///</summary>
public static WcfInstanceContext Current
{
get
{
WcfInstanceContext context = OperationContext.Current.InstanceContext.Extensions.Find<WcfInstanceContext>();
if (context == null)
{
context = new WcfInstanceContext();
OperationContext.Current.InstanceContext.Extensions.Add(context);
}
return context;
}
}
/// <summary>
/// <see cref="IExtension{T}"/> Attach() method
/// </summary>
public void Attach(InstanceContext owner) { }
/// <summary>
/// <see cref="IExtension{T}"/> Detach() method
/// </summary>
public void Detach(InstanceContext owner) { }
}
}
registered the following way:
<extensions>
<behaviorExtensions>
<add name="WcfInstanceContext" type="MyTimeService.WcfExtension, MyTimeService.WcfExtension.WcfInstanceContext" />
</behaviorExtensions>
</extensions>
Then I created a custom ServiceBehavior
public class WcfSessionPerRequestBehavior : IServiceBehavior
{
private ISessionFactoryProvider _sfp;
public WcfSessionPerRequestBehavior(ISessionFactoryProvider sfp)
{
_sfp = sfp;
}
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (var cdb in serviceHostBase.ChannelDispatchers)
{
var channelDispatcher = cdb as ChannelDispatcher;
if (null != channelDispatcher)
{
foreach (var endpointDispatcher in channelDispatcher.Endpoints)
{
foreach (var dispatchOperation in endpointDispatcher.DispatchRuntime.Operations)
{
dispatchOperation.CallContextInitializers.Add(new WcfSessionPerRequestCallContextInitializer(_sfp));
}
}
}
}
}
followed by a custom ICallContextInitializer:
public class WcfSessionPerRequestCallContextInitializer : ICallContextInitializer
{
private ILogger logger = NullLogger.Instance;
public ILogger Logger
{
get { return logger; }
set { logger = value; }
}
private ISessionFactoryProvider sfp;
public WcfSessionPerRequestCallContextInitializer(ISessionFactoryProvider s)
{
this.sfp = s;
}
public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
{
foreach (var sf in sfp.GetSessionFactories())
{
var localFactory = sf;
LazySessionContext.Bind(new Lazy<NHibernate.ISession>(() => BeginSession(localFactory)), sf);
}
return null;
}
public void AfterInvoke(object correlationState)
{
foreach (var sf in sfp.GetSessionFactories())
{
var session = LazySessionContext.UnBind(sf);
if (session == null) continue;
EndSession(session);
}
}
private static NHibernate.ISession BeginSession(ISessionFactory sf)
{
var session = sf.OpenSession();
session.BeginTransaction();
return session;
}
private void ContextEndRequest(object sender, EventArgs e)
{
foreach (var sf in sfp.GetSessionFactories())
{
var session = LazySessionContext.UnBind(sf);
if (session == null) continue;
EndSession(session);
}
}
private static void EndSession(NHibernate.ISession session)
{
if (session.Transaction != null && session.Transaction.IsActive)
{
session.Transaction.Commit();
}
session.Dispose();
}
}
and at last I adjusted the ICurrentSessionContext:
public class LazySessionContext : ICurrentSessionContext
{
private readonly ISessionFactoryImplementor factory;
private const string CurrentSessionContextKey = "NHibernateCurrentSession";
public LazySessionContext(ISessionFactoryImplementor factory)
{
this.factory = factory;
}
/// <summary>
/// Retrieve the current session for the session factory.
/// </summary>
/// <returns></returns>
public NHibernate.ISession CurrentSession()
{
Lazy<NHibernate.ISession> initializer;
var currentSessionFactoryMap = GetCurrentFactoryMap();
if (currentSessionFactoryMap == null || !currentSessionFactoryMap.TryGetValue(factory, out initializer))
{
return null;
}
return initializer.Value;
}
/// <summary>
/// Bind a new sessionInitializer to the context of the sessionFactory.
/// </summary>
/// <param name="sessionInitializer"></param>
/// <param name="sessionFactory"></param>
public static void Bind(Lazy<NHibernate.ISession> sessionInitializer, ISessionFactory sessionFactory)
{
var map = GetCurrentFactoryMap();
map[sessionFactory] = sessionInitializer;
}
/// <summary>
/// Unbind the current session of the session factory.
/// </summary>
/// <param name="sessionFactory"></param>
/// <returns></returns>
public static NHibernate.ISession UnBind(ISessionFactory sessionFactory)
{
var map = GetCurrentFactoryMap();
var sessionInitializer = map[sessionFactory];
map[sessionFactory] = null;
if (sessionInitializer == null || !sessionInitializer.IsValueCreated) return null;
return sessionInitializer.Value;
}
/// <summary>
/// Provides the CurrentMap of SessionFactories.
/// If there is no map create/store and return a new one.
/// </summary>
/// <returns></returns>
private static IDictionary<ISessionFactory, Lazy<NHibernate.ISession>> GetCurrentFactoryMap()
{
//var currentFactoryMap = (IDictionary<ISessionFactory, Lazy<NHibernate.ISession>>)HttpContext.Current.Items[CurrentSessionContextKey];
var currentFactoryMap = (IDictionary<ISessionFactory, Lazy<NHibernate.ISession>>)WcfInstanceContext.Current.Items[CurrentSessionContextKey];
if (currentFactoryMap == null)
{
currentFactoryMap = new Dictionary<ISessionFactory, Lazy<NHibernate.ISession>>();
WcfInstanceContext.Current.Items[CurrentSessionContextKey] = currentFactoryMap;
}
return currentFactoryMap;
}
}
This seems to work but since I'm new to all that stuff I can't say if I did this corretly.
Can anyone have a look at it and give me feedback?
Cheers,
Stefan
Your are using OperationContext.Current which is the correct way to do per request context implementations for WCF services, so it looks good to me...
Question is, why are you not simply using the default implementation which comes out of the box with nhibernate? The implementation is in NHibernate.Context.WcfOperationSessionContext and you would just have to use this one within your session factory setup
For example:
Fluently.Configure()
...
.ExposeConfiguration(cfg => cfg.SetProperty(
Environment.CurrentSessionContextClass,
"wcf")
or Fluently.Configure()...CurrentSessionContext<WcfOperationSessionContext>()
You could also simply setaspNetCompatibilityEnabled=true and you will get HttpContext available to both MVC and WCF.
Related
Describe the bug
After upgrading from .net core 2.2 to 3.1, integration tests are failing.
All tests are wrapped in TransactionScope so that all changes to db should be revered (scope.Complete() is not called).
When call to the data access layer is made through api (HttpClient) records are created in the database, but they should not be since the entire test is wrapped in TransactionScope.
To Reproduce
public class Entity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CustomDbContext : DbContext
{
private const string DefaultConnectionString = "Server=.;Initial Catalog=WebApi;Trusted_Connection=True;";
private readonly string _connectionString;
public CustomDbContext() : this(DefaultConnectionString)
{
}
public CustomDbContext(string connectionString)
{
_connectionString = connectionString;
}
public DbSet<Entity> Entities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new EntityConfiguration());
}
public async Task Save<TModel>(TModel model)
{
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
{
Update(model);
await SaveChangesAsync();
scope.Complete();
}
}
}
public class EntityService : IEntityService
{
private readonly CustomDbContext _db;
public EntityService(CustomDbContext db)
{
_db = db;
}
public async Task Save(Entity model) => await _db.Save(model);
}
[ApiController]
[Route("[controller]")]
public class EntityController : ControllerBase
{
private readonly IEntityService _service;
public EntityController(IEntityService service)
{
_service = service;
}
[HttpPost]
public async Task<IActionResult> Save(Entity model)
{
await _service.Save(model);
return Ok();
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<CustomDbContext>();
services.AddScoped<IEntityService, EntityService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
/// <summary>
/// Apply this attribute to your test method to automatically create a <see cref="TransactionScope"/>
/// that is rolled back when the test is finished.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AutoRollbackAttribute : BeforeAfterTestAttribute
{
TransactionScope scope;
/// <summary>
/// Gets or sets whether transaction flow across thread continuations is enabled for TransactionScope.
/// By default transaction flow across thread continuations is enabled.
/// </summary>
public TransactionScopeAsyncFlowOption AsyncFlowOption { get; set; } = TransactionScopeAsyncFlowOption.Enabled;
/// <summary>
/// Gets or sets the isolation level of the transaction.
/// Default value is <see cref="IsolationLevel"/>.Unspecified.
/// </summary>
public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.Unspecified;
/// <summary>
/// Gets or sets the scope option for the transaction.
/// Default value is <see cref="TransactionScopeOption"/>.Required.
/// </summary>
public TransactionScopeOption ScopeOption { get; set; } = TransactionScopeOption.Required;
/// <summary>
/// Gets or sets the timeout of the transaction, in milliseconds.
/// By default, the transaction will not timeout.
/// </summary>
public long TimeoutInMS { get; set; } = -1;
/// <summary>
/// Rolls back the transaction.
/// </summary>
public override void After(MethodInfo methodUnderTest)
{
scope.Dispose();
}
/// <summary>
/// Creates the transaction.
/// </summary>
public override void Before(MethodInfo methodUnderTest)
{
var options = new TransactionOptions { IsolationLevel = IsolationLevel };
if (TimeoutInMS > 0)
options.Timeout = TimeSpan.FromMilliseconds(TimeoutInMS);
scope = new TransactionScope(ScopeOption, options, AsyncFlowOption);
}
}
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
private const string TestDbConnectionString = "Server=.;Initial Catalog=WebApiTestDB_V3;Trusted_Connection=True;";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton(_ => new CustomDbContext(TestDbConnectionString));
var sp = services.BuildServiceProvider();
var db = sp.GetRequiredService<CustomDbContext>();
db.Database.Migrate();
});
}
}
public class IntegrationTest : IClassFixture<CustomWebApplicationFactory>
{
protected readonly HttpClient _client;
protected readonly IServiceProvider _serviceProvider;
protected readonly CustomDbContext _db;
public IntegrationTest(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
_serviceProvider = factory.Services.CreateScope().ServiceProvider;
_db = _serviceProvider.GetRequiredService<CustomDbContext>();
}
protected void DetachAll()
{
_db.ChangeTracker.Entries()
.ToList()
.ForEach(e => e.State = EntityState.Detached);
}
protected async Task<Entity> AddTestEntity()
{
var model = new Entity
{
Name = "test entity"
};
await _db.AddAsync(model);
await _db.SaveChangesAsync();
return model;
}
}
public static class HttpContentHelper
{
public static HttpContent GetJsonContent(object model) =>
new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
}
[AutoRollback]
public class EntityIntegrationTest : IntegrationTest
{
private const string apiUrl = "/entity";
public EntityIntegrationTest(CustomWebApplicationFactory factory) : base(factory)
{
}
[Fact]
public async Task CanAdd()
{
// arrange
var model = new Entity
{
Name = "new entity"
};
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
response.EnsureSuccessStatusCode();
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CanUpdate()
{
// arrange
var model = await AddTestEntity();
DetachAll(); // detach all entries because posting to api would create a new model, saving a new object with existing key throws entity already tracked exception
model.Name = "updated entity";
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
response.EnsureSuccessStatusCode();
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Id, result.Id);
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CannotInsertDuplicate()
{
// arrange
var entity = await AddTestEntity();
var model = new Entity
{
Name = entity.Name
};
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
var result = await response.Content.ReadAsStringAsync();
Assert.Contains("Cannot insert duplicate", result);
}
}
There are many files/classes involved so I've created a example repository
Example tests that are failing are in https://github.com/niksloter74/web-api-integration-test/tree/master/netcore3.1
Working example in .net core 2.2 https://github.com/niksloter74/web-api-integration-test/tree/master/netcore2.2
Direct test for service layer is working correctly
[AutoRollback]
public class EntityServiceTest : IntegrationTest
{
private readonly IEntityService service;
public EntityServiceTest(CustomWebApplicationFactory factory) : base(factory)
{
service = _serviceProvider.GetRequiredService<IEntityService>();
}
[Fact]
public async Task CanAdd()
{
// arrange
var model = new Entity
{
Name = "new entity"
};
// act
await service.Save(model);
// assert
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CanUpdate()
{
// arrange
var model = await AddTestEntity();
model.Name = "updated entity";
// act
await service.Save(model);
// assert
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Id, result.Id);
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CannotInsertDuplicate()
{
// arrange
var entity = await AddTestEntity();
var model = new Entity
{
Name = entity.Name
};
// act
var ex = await Assert.ThrowsAnyAsync<Exception>(async () => await service.Save(model));
// assert
Assert.StartsWith("Cannot insert duplicate", ex.InnerException.Message);
}
}
This is by design but there’s a flag to get the old behavior back on TestServer called PreserveExecutionContext.
Here is an official discussion thread.
This line in IntegartionTest class fixed the problem
_factory.Server.PreserveExecutionContext = true;
I've also updated the repository
i'm working in a new project and i'm trying to implement nhibernate with ninject in asp.net mvc. I'm having the error:
NHibernate.HibernateException: No session bound to the current context
Below is the code i'm using:
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Conventions.Helpers;
using NHibernate;
using NHibernate.Context;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace TestNhibernateSessions
{
public class SessionFactory : IHttpModule
{
private static readonly ISessionFactory _SessionFactory;
static SessionFactory()
{
_SessionFactory = CreateSessionFactory();
}
public void Init(HttpApplication context)
{
context.BeginRequest += BeginRequest;
context.EndRequest += EndRequest;
}
public void Dispose()
{
}
public static ISession GetCurrentSession()
{
return _SessionFactory.GetCurrentSession();
}
private static void BeginRequest(object sender, EventArgs e)
{
ISession session = _SessionFactory.OpenSession();
session.BeginTransaction();
CurrentSessionContext.Bind(session);
}
private static void EndRequest(object sender, EventArgs e)
{
ISession session = CurrentSessionContext.Unbind(_SessionFactory);
if (session == null) return;
try
{
session.Transaction.Commit();
}
catch (Exception)
{
session.Transaction.Rollback();
}
finally
{
session.Close();
session.Dispose();
}
}
private static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromConnectionStringWithKey("DefaultConnection")))
.Mappings(m => m.AutoMappings.Add(CreateMappings()))
.CurrentSessionContext<WebSessionContext>()
.BuildSessionFactory();
}
private static AutoPersistenceModel CreateMappings()
{
return AutoMap
.Assembly(System.Reflection.Assembly.GetCallingAssembly())
.Where(t => t.Namespace != null && t.Namespace.EndsWith("Domain"))
.Conventions.Setup(c => c.Add(DefaultCascade.SaveUpdate()));
}
}
}
[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(TestNhibernateSessions.App_Start.NinjectWebCommon), "Start")]
[assembly: WebActivatorEx.ApplicationShutdownMethodAttribute(typeof(TestNhibernateSessions.App_Start.NinjectWebCommon), "Stop")]
namespace TestNhibernateSessions.App_Start
{
using System;
using System.Web;
using Microsoft.Web.Infrastructure.DynamicModuleHelper;
using Ninject;
using Ninject.Web.Common;
using TestNhibernateSessions.Repository;
using TestNhibernateSessions.Service;
using NHibernate;
public static class NinjectWebCommon
{
private static readonly Bootstrapper bootstrapper = new Bootstrapper();
/// <summary>
/// Starts the application
/// </summary>
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
bootstrapper.Initialize(CreateKernel);
}
/// <summary>
/// Stops the application.
/// </summary>
public static void Stop()
{
bootstrapper.ShutDown();
}
/// <summary>
/// Creates the kernel that will manage your application.
/// </summary>
/// <returns>The created kernel.</returns>
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
try
{
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
return kernel;
}
catch
{
kernel.Dispose();
throw;
}
}
/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IProductRepository>().To<ProductRepository>();
kernel.Bind<IProductService>().To<ProductService>();
}
}
}
Since you're using Ninject, my recommendation is to use it to inject the session factory rather than an IHttpModule. To do so, create Ninject Provider classes as shown below. Note that this requires transaction management in code, I dislike the idea of blindly holding a transaction open during a request.
public class SessionFactoryProvider : Provider<ISessionFactory>
{
protected override ISessionFactory CreateInstance(IContext context)
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromConnectionStringWithKey("DefaultConnection")))
.Mappings(m => m.AutoMappings.Add(CreateMappings()))
.CurrentSessionContext<WebSessionContext>()
.BuildSessionFactory();
}
}
public class SessionProvider : Provider<ISession>
{
protected override ISession CreateInstance(IContext context)
{
var sessionFactory = context.Kernel.Get<ISessionFactory>();
var session = sessionFactory.OpenSession();
session.FlushMode = FlushMode.Commit;
return session;
}
}
Then in NinjectWebCommon:
private static void RegisterServices(IKernel kernel)
{
// session factory instances are singletons
kernel.Bind<ISessionFactory>().ToProvider<SessionFactoryProvider>().InSingletonScope();
// session-per-request
kernel.Bind<ISession>().ToProvider<SessionProvider>().InRequestScope();
}
I´m developing an ASP.NET MVC Application, in which I use NHibernate and Ninject.
The Problem is caused by the following Controller:
public class ShoppingCartController : Controller
{
private readonly Data.Infrastructure.IShoppingCartRepository _shoppingCartRepository;
private readonly Data.Infrastructure.IShopItemRepository _shopItemRepository;
public ShoppingCartController(Data.Infrastructure.IShoppingCartRepository shoppingCartController,
Data.Infrastructure.IShopItemRepository shopItemRepository)
{
_shoppingCartRepository = shoppingCartController;
_shopItemRepository = shopItemRepository;
}
public ActionResult AddToShoppingCart(FormCollection formCollection)
{
var cartItem = new Data.Models.ShoppingCartItem();
cartItem.ChangeDate = DateTime.Now;
cartItem.ShopItem = _shopItemRepository.GetShopItem(SessionData.Data.Info, Convert.ToInt32(formCollection["shopItemId"]));
//IF I DONT´T CALL THE METHOD ABOVE, AddToCart works
_shoppingCartRepository.AddToCart(SessionData.Data.Info, cartItem);
//BUT IF I CALL THE GetShopItem METHOD I GET THE EXCEPTION HERE!
return RedirectToAction("Index", "Shop");
}
}
I know most of the Time this Exception is caused by wrong Mapping, but I´m pretty sure that my Mapping is right because the AddToCart-Method works if I don´t call GetShopItem...
So here is the Code of the ShopItemRepository:
public class ShopItemRepository : ReadOnlyRepository<ShopItem>, IShopItemRepository
{
public ShopItemRepository(IUnitOfWork uow) : base(uow)
{
}
public ShopItem GetShopItem(SessionParams param, int id)
{
return CurrentSession.QueryOver<ShopItem>()
.Where(x => x.ProcessId == param.ProcessId &&
x.CatalogueId == param.CatalogueId &&
x.Id == id)
.SingleOrDefault();
}
public IList<ShopItem> GetShopItems(SessionParams param)
{
return CurrentSession.GetNamedQuery("GetShopItems")
.SetParameter("requestor_id", param.RequestorId)
.SetParameter("recipient_id", param.RecipientId)
.SetParameter("process_id", param.ProcessId)
.SetParameter("catalogue_id", param.CatalogueId)
.List<ShopItem>();
}
}
And finally the Code of my UnitOfWork (basically it is just a Wrapper for the Session because I don´t want to reference NHibernate in my MVC Project)
public class UnitOfWork : IUnitOfWork, IDisposable
{
private NHibernate.ISession _currentSession;
public NHibernate.ISession CurrentSession
{
get
{
if(_currentSession == null)
{
_currentSession = SessionFactoryWrapper.SessionFactory.OpenSession();
}
return _currentSession;
}
}
public void Dispose()
{
if(_currentSession != null)
{
_currentSession.Close();
_currentSession.Dispose();
_currentSession = null;
}
GC.SuppressFinalize(this);
}
}
Addendum:
My NinjectWebCommon Class
public static class NinjectWebCommon
{
private static readonly Bootstrapper bootstrapper = new Bootstrapper();
/// <summary>
/// Starts the application
/// </summary>
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
bootstrapper.Initialize(CreateKernel);
}
/// <summary>
/// Stops the application.
/// </summary>
public static void Stop()
{
bootstrapper.ShutDown();
}
/// <summary>
/// Creates the kernel that will manage your application.
/// </summary>
/// <returns>The created kernel.</returns>
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
return kernel;
}
/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IUnitOfWork>().To<UnitOfWork>().InRequestScope();
kernel.Bind<Data.Infrastructure.ICatalogueRepository>().To<Data.Repositories.CatalogueRepository>();
kernel.Bind<Data.Infrastructure.ICategoryRepository>().To<Data.Repositories.CategoryRepository>();
kernel.Bind<Data.Infrastructure.IContactRepository>().To<Data.Repositories.ContactRepository>();
kernel.Bind<Data.Infrastructure.IProcessRepository>().To<Data.Repositories.ProcessRepository>();
kernel.Bind<Data.Infrastructure.IShopItemRepository>().To<Data.Repositories.ShopItemRepository>();
kernel.Bind<Data.Infrastructure.IShoppingCartRepository>().To<Data.Repositories.ShoppingCartRepository>();
}
}
IUnitOfWork is set to RequestScope so in the Case of ShoppingCartController, the two Repositories share the same UOW right?
Maybe this could cause the Problem?
Are you sure that this isn´t caused by wrong mapping? I had the same Issue and could resolve it by checking my mappings again!
I'm subclassing the AuthorizeAttribute so I can implement token authentication whereby the token is passed in the request header. I'm also using Ninject for IoC. The overriden OnAuthorization method gets called and validates the token but I still receive a 401.
Any ideas on why this is happening?
TokenAuthorisationAttribute.cs
public class TokenAuthorisationAttribute : AuthorizeAttribute
{
private readonly IRepository _repository;
public TokenAuthorisationAttribute(IRepository repository)
{
_repository = repository;
}
public override void OnAuthorization(
HttpActionContext actionContext)
{
if (!ValidateToken(actionContext.ControllerContext.Request))
HandleUnauthorizedRequest(actionContext);
base.OnAuthorization(actionContext);
}
private bool ValidateToken(HttpRequestMessage request)
{
const string authenticationToken = "Authentication-Token";
var token = request.Headers.GetValues(authenticationToken).FirstOrDefault();
if (token == null)
return false;
var device = _repository.FindSingleOrDefault<Device>(x => x.Id.Equals(token));
if (device == null || !token.Equals(device.Id))
return false;
return true;
}
}
NinjectWebCommon.cs
public static class NinjectWebCommon
{
private static readonly Bootstrapper Bootstrapper = new Bootstrapper();
/// <summary>
/// Starts the application
/// </summary>
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
Bootstrapper.Initialize(CreateKernel);
}
/// <summary>
/// Stops the application.
/// </summary>
public static void Stop()
{
Bootstrapper.ShutDown();
}
/// <summary>
/// Creates the kernel that will manage your application.
/// </summary>
/// <returns>The created kernel.</returns>
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
GlobalConfiguration.Configuration.DependencyResolver = new NinjectDependencyResolver(kernel);
return kernel;
}
/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
var connectionString = ConfigurationManager.ConnectionStrings["MONGOHQ_URL"].ConnectionString;
var databaseName = ConfigurationManager.AppSettings["Database"];
kernel.Bind<IRepository>().To<MongoRepository>()
.WithConstructorArgument("connectionString", connectionString)
.WithConstructorArgument("databaseName", databaseName);
kernel.BindHttpFilter<TokenAuthorisationAttribute>(FilterScope.Global);
}
I managed to resolve this issue by overriding the IsAuthorized method instead of OnAuthorization. Not 100% sure if this is the right approach? Any opinions ??
public class TokenAuthorisationAttribute : AuthorizeAttribute
{
private readonly IRepository _repository;
public TokenAuthorisationAttribute(IRepository repository)
{
_repository = repository;
}
protected override bool IsAuthorized(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Authorization == null)
return false;
var authToken = actionContext.Request.Headers.Authorization.Parameter;
var decodedToken = Encoding.UTF8.GetString(Convert.FromBase64String(authToken));
var deviceToken = new DeviceToken(decodedToken);
var device = _repository.FindSingleOrDefault<Device>(x => x.Id.Equals(deviceToken.GetDeviceId()));
if (device != null)
{
HttpContext.Current.User = new GenericPrincipal(new ApiIdentity(device), new string[] {});
return true;
}
return base.IsAuthorized(actionContext);
}
}
The ASP.NET WebAPI is an open source project. So you can read the correspondingly codes here:
AuthorizationFilterAttribute http://aspnetwebstack.codeplex.com/SourceControl/changeset/view/03357424caf5#src%2fSystem.Web.Http%2fFilters%2fAuthorizationFilterAttribute.cs
AuthorizeAttribute
http://aspnetwebstack.codeplex.com/SourceControl/changeset/view/03357424caf5#src%2fSystem.Web.Http%2fAuthorizeAttribute.cs
There are two facts an AuthorizationFilterAttribute takes in to consider when it is making decision:
Is OnAuthorization throw exception; or
Is the actionContext's Response field is filled;
Either one is fulfilled, the remaining action filters and action are shortcut.
Based on your code, I'm curious if the function HandleUnauthorizedRequest does either of the operation above.
The reason why overriding IsAuthorized works, is because it works at AuthorizeAttribute level. The OnAuthorization call to the IsAuthorized overload and set value to actionContext's Request property.
Thanks,
Troy
I am pretty new with DI and IoC pattern.
public class LazySessionContext
{
private readonly ISessionFactoryImplementor factory;
private const string CurrentSessionContextKey = "NHibernateCurrentSession";
public LazySessionContext(ISessionFactoryImplementor factory)
{
this.factory = factory;
}
/// <summary>
/// Retrieve the current session for the session factory.
/// </summary>
/// <returns></returns>
public ISession CurrentSession()
{
Lazy<ISession> initializer;
var currentSessionFactoryMap = GetCurrentFactoryMap();
if (currentSessionFactoryMap == null ||
!currentSessionFactoryMap.TryGetValue(factory, out initializer))
{
return null;
}
return initializer.Value;
}
/// <summary>
/// Bind a new sessionInitializer to the context of the sessionFactory.
/// </summary>
/// <param name="sessionInitializer"></param>
/// <param name="sessionFactory"></param>
public static void Bind(Lazy<ISession> sessionInitializer, ISessionFactory sessionFactory)
{
var map = GetCurrentFactoryMap();
map[sessionFactory] = sessionInitializer;
}
/// <summary>
/// Unbind the current session of the session factory.
/// </summary>
/// <param name="sessionFactory"></param>
/// <returns></returns>
public static ISession UnBind(ISessionFactory sessionFactory)
{
var map = GetCurrentFactoryMap();
var sessionInitializer = map[sessionFactory];
map[sessionFactory] = null;
if (sessionInitializer == null || !sessionInitializer.IsValueCreated) return null;
return sessionInitializer.Value;
}
/// <summary>
/// Provides the CurrentMap of SessionFactories.
/// If there is no map create/store and return a new one.
/// </summary>
/// <returns></returns>
private static IDictionary<ISessionFactory, Lazy<ISession>> GetCurrentFactoryMap()
{
var currentFactoryMap = (IDictionary<ISessionFactory, Lazy<ISession>>)
HttpContext.Current.Items[CurrentSessionContextKey];
if (currentFactoryMap == null)
{
currentFactoryMap = new Dictionary<ISessionFactory, Lazy<ISession>>();
HttpContext.Current.Items[CurrentSessionContextKey] = currentFactoryMap;
}
return currentFactoryMap;
}
}
public interface ISessionFactoryProvider
{
IEnumerable<ISessionFactory> GetSessionFactories();
}
public class SessionFactoryProvider
{
public const string Key = "NHibernateSessionFactoryProvider";
}
public class NHibernateSessionModule : IHttpModule
{
private HttpApplication app;
public void Init(HttpApplication context)
{
app = context;
context.BeginRequest += ContextBeginRequest;
context.EndRequest += ContextEndRequest;
context.Error += ContextError;
}
private void ContextBeginRequest(object sender, EventArgs e)
{
var sfp = (ISessionFactoryProvider)app.Context.Application[SessionFactoryProvider.Key];
foreach (var sf in sfp.GetSessionFactories())
{
var localFactory = sf;
LazySessionContext.Bind(
new Lazy<ISession>(() => BeginSession(localFactory)),
sf);
}
}
private static ISession BeginSession(ISessionFactory sf)
{
var session = sf.OpenSession();
session.BeginTransaction();
return session;
}
private void ContextEndRequest(object sender, EventArgs e)
{
var sfp = (ISessionFactoryProvider)app.Context.Application[SessionFactoryProvider.Key];
var sessionsToEnd = sfp.GetSessionFactories()
.Select(LazySessionContext.UnBind)
.Where(session => session != null);
foreach (var session in sessionsToEnd)
{
EndSession(session);
}
}
private void ContextError(object sender, EventArgs e)
{
var sfp = (ISessionFactoryProvider)app.Context.Application[SessionFactoryProvider.Key];
var sessionstoAbort = sfp.GetSessionFactories()
.Select(LazySessionContext.UnBind)
.Where(session => session != null);
foreach (var session in sessionstoAbort)
{
EndSession(session, true);
}
}
private static void EndSession(ISession session, bool abort = false)
{
if (session.Transaction != null && session.Transaction.IsActive)
{
if (abort)
{
session.Transaction.Rollback();
}
else
{
session.Transaction.Commit();
}
}
session.Dispose();
}
public void Dispose()
{
app.BeginRequest -= ContextBeginRequest;
app.EndRequest -= ContextEndRequest;
app.Error -= ContextError;
}
}
I got this sample by chinooknugets and jfmarillo from GitHub. From the code above I am injecting the session into repository and controlling the transaction via IHttpmodule. Now there are two concerns for me:
If I implement the transaction management via the code above it would be called whenever there is a request and it would open a session for that request. That's the sole purpose of implementing "Session Per Request" approach. But what if among all my controller methods I have only one method which actually uses the repository then I don't want to open the session every time I make a request. Only during the actions which are marked by Transaction attribute in controller would handle the transaction.
I would make a request and session would be opened only when the repository requests for it, so can it be implemented via any IoC container.
I still want the httpcontext events to handle the transaction so that context+=BegingRequest and context+=EndRequest. and the transaction would be handled inside that httpmodule with the httpcontext on request. But I don't want to implement IhttpModule and put into web.config. Is there any other alternative for this approach ?
The session opening and closing would be done solely inside those httpcontext only however I want to manage it via an IoC container (preferably ninject) but only when the repository is being requesting for that session. Mind it the repository may be initialised when the controller is invoked but that shouldn't open the session inside that repository. The session should open when actually repository is performing any transient actions.
Would someone clarify what practise should I follow for this scenario? I am using Mvc 3 with ninject and nhibernate.
1, 2)
Ninject allows Lazy object creation using Ninject.Extensions.Factory 3.0.0
class MyController
{
public MyController(Lazy<SomeRepository> repository) { ... }
}
This way the repository (and the context) is created when used.
3)
Why do you want to use a HttpModule for this? There are much easier ways e.g.:
kernel.Bind<ISession>()
.ToMethod(ctx => ctx.Kernel.Get<ISessionFactory().OpenSession())
.InRequestScope()
.OnActivation(session => OpenTransaction(session))
.OnDeactivation(session => EndTransaction(session));
Starting from Ninject 3.0.0 you can add a binding for HttpModules instead of registering them in the web.config and use construcotr injection for them. But since the HttpModule has no knowledge if the context is used you have to open the transaction for all requests.