Sorted By: Tag (ravendb)

AspNet.Identity.RavenDB: Fully asynchronous, new and sweet ASP.NET Identity implementation for RavenDB

A while back, ASP.NET team has introduced ASP.NET Identity, a membership system for ASP.NET applications. Today, I'm introducing you its RavenDB implementation: AspNet.Identity.RavenDB.
2013-11-29 09:39
Tugberk Ugurlu


A while back, ASP.NET team has introduced ASP.NET Identity, a membership system for ASP.NET applications. If you have Visual Studio 2013 installed on you box, you will see ASP.NET Identity Core and ASP.NET Identity Entity Framework libraries are pulled down when you create a new Web Application by configuring the authentication as "Individual User Accounts".

image

After creating your MVC project, you will see that you have an AccountController which a completely different code from the previous project templates as it uses ASP.NET Identity.

You can find tons of information about this new membership system from ASP.NET Identity section on official ASP.NET web site. Also, Pranav Rastogi (a.k.a @rustd) has a great introduction video on ASP.NET Identity which you shouldn't miss for sure.

One of the great features of ASP.NET Identity system is the fact that it is super extensible. The core layer and the implementation layer (which is Entity Framework by default) are decouple from each other. This means that you are not bound to Entity Framework and SQL Server for storage. You can implement ASP.NET Identity on your choice of storage system. This is exactly what I did and I created AspNet.Identity.RavenDB project which is fully asynchronous, new and sweet ASP.NET Identity implementation for RavenDB. You can install this library from NuGet:

PM> Install-Package AspNet.Identity.RavenDB

Getting started with AspNet.Identity.RavenDB is also really easy. Just create an ASP.NET MVC application from scratch by configuring the authentication as "Individual User Accounts". Then, install the AspNet.Identity.RavenDB package. As the default project is set to work with ASP.NET Identity Entity Framework implementation, you need to make a few more tweak here and there to make it work with RavenDB.

First, open the IdentityModels.cs file under the "Models" folder and delete the two classes you see there. Only class you need is the following ApplicationUser class:

public class ApplicationUser : RavenUser
{
}

As the second step, open up the AccountController.cs file under the "Controllers" folder and have delete the first constructor you see there. Only constructor you need is the following one:

public AccountController(UserManager<ApplicationUser> userManager)
{
    UserManager = userManager;
}

Now you should be able to build the project successfully and from that point on, you can uninstall the Microsoft.AspNet.Identity.EntityFramework package which you don't need anymore. Lastly, we need to provide an instance of UserManager<ApplicationUser> to our account controller. I'm going to use Autofac IoC container for that operation to inject the dependency into my project. However, you can choose any IoC container you like. After I install the Autofac.Mvc5 package, here how my Global class looks like inside Global.asax.cs file:

using AspNet.Identity.RavenDB.Stores;
using AspNetIndetityRavenDb.Models;
using Autofac;
using Autofac.Integration.Mvc;
using Microsoft.AspNet.Identity;
using Raven.Client;
using Raven.Client.Document;
using Raven.Client.Extensions;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace AspNetIndetityRavenDb
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            const string RavenDefaultDatabase = "Users";
            ContainerBuilder builder = new ContainerBuilder();
            builder.Register(c =>
            {
                IDocumentStore store = new DocumentStore
                {
                    Url = "http://localhost:8080",
                    DefaultDatabase = RavenDefaultDatabase
                }.Initialize();

                store.DatabaseCommands.EnsureDatabaseExists(RavenDefaultDatabase);

                return store;

            }).As<IDocumentStore>().SingleInstance();

            builder.Register(c => c.Resolve<IDocumentStore>()
                .OpenAsyncSession()).As<IAsyncDocumentSession>().InstancePerHttpRequest();
            builder.Register(c => new RavenUserStore<ApplicationUser>(c.Resolve<IAsyncDocumentSession>(), false))
                .As<IUserStore<ApplicationUser>>().InstancePerHttpRequest();
            builder.RegisterType<UserManager<ApplicationUser>>().InstancePerHttpRequest();

            builder.RegisterControllers(Assembly.GetExecutingAssembly());

            DependencyResolver.SetResolver(new AutofacDependencyResolver(builder.Build()));
        }
    }
}

Before starting up my application, I expose my RavenDB engine through http://localhost:8080. and I'm all set to fly right now. The default project template allows me to register and log in the application and we can perform all those actions now.

2

The same sample application available inside the repository as well if you are interested in: AspNet.Identity.RavenDB.Sample.Mvc.

The current ASP.NET Identity system doesn't provide that many features which we require in real world applications such as account confirmation, password reset but it provides us a really great infrastructure and the UserManager<TUser> class saves us from writing bunch of code. I'm sure we will see all other implementations of ASP.NET Identity such as MongoDB, Windows Azure Table Storage, etc. from the community.

Turkish I Problem on RavenDB and Solving It with Custom Lucene Analyzers

Yesterday, I ran into a Turkish I problem on RavenDB and here is how I solved It with a custom Lucene analyzer
2013-07-16 14:37
Tugberk Ugurlu


RavenDB, by default, uses a custom Lucene.Net analyzer named LowerCaseKeywordAnalyzer and it makes all your queries case-insensitive which is what I would expect. For example, the following query find the User whose name property is set to "TuGbErK":

class Program
{
     static void Main(string[] args)
     {
          const string DefaultDatabase = "EqualsTryOut";
          IDocumentStore store = new DocumentStore
          {
               Url = "http://localhost:8080",
               DefaultDatabase = DefaultDatabase
          }.Initialize();
          store.DatabaseCommands.EnsureDatabaseExists(DefaultDatabase);
          using (var ses = store.OpenSession())
          {
               var user = new User { 
                  Name = "TuGbErK", 
                  Roles = new List<string> { "adMin", "GuEst" } 
               };
               
               ses.Store(user);
               ses.SaveChanges();
               //this finds name:TuGbErK
               var user1 = ses.Query<User>()
                  .Where(usr => usr.Name == "tugberk")
                  .FirstOrDefault();
          }
     }
}
public class User
{
     public string Id { get; set; }
     public string Name { get; set; }
     public ICollection<string> Roles { get; set; }
}

The problem starts appearing here where you have a situation that requires you to store Turkish text. You may ask why at this point, which makes sense. The problem is related to well-known Turkish "I" problem. Let's try to produce this problem with an example on RavenDB.

class Program
{
    static void Main(string[] args)
    {
        const string DefaultDatabase = "EqualsTryOut";
        IDocumentStore store = new DocumentStore
        {
            Url = "http://localhost:8080",
            DefaultDatabase = DefaultDatabase
        }.Initialize();

        store.DatabaseCommands.EnsureDatabaseExists(DefaultDatabase);

        using (var ses = store.OpenSession())
        {
            var user = new User { 
                Name = "Irmak", 
                Roles = new List<string> { "adMin", "GuEst" } 
            };
            
            ses.Store(user);
            ses.SaveChanges();

            //This fails dues to Turkish I
            var user1 = ses.Query<User>()
                .Where(usr => usr.Name == "irmak")
                .FirstOrDefault();

            //this finds name:Irmak
            var user2 = ses.Query<User>()
                .Where(usr => usr.Name == "IrMak")
                .FirstOrDefault();
        }
    }
}

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
    public ICollection<string> Roles { get; set; }
}

Here, we have the same code but the Name value we are storing is different: Irmak. Irmak is a Turkish name which also means "river" in English (which is totally not the point here) and it starts with the Turkish letter "I". The lowercased version of this letter is "ı" and this is where the problem arises because if you are lowercasing this character in an invariant culture manner, it will be transformed as "i", not "ı". This is what RavenDB is doing with its LowerCaseKeywordAnalyzer and that's why we can't find anything with the first query above where we searched against "ırmak". In the second query, we can find the User that we are looking for as it will be lowercased into "irmak".

The Solution with a Custom Analyzer

The default analyzer that RavenDB using is the LowerCaseKeywordAnalyzer and it uses the LowerCaseKeywordTokenizer as its tokenizer. Inside that tokenizer, you will see the Normalize method which is used to lowercase a character in an invariant culture manner which causes a problem on our case here. AFAIK, there is no built in Lucene.Net tokenizer which handles Turkish characters well (I might be wrong here). So, I decided to modify the LowerCaseKeywordTokenizer according to my specific needs. It was a very naive and minor change which worked but not sure if I handled it well. You can find the source code of the TutkishLowerCaseKeywordTokenizer and TurkishLowerCaseKeywordAnalyzer classes on my Github repository.

Using a Custom Build Analyzer on RavenDB

RavenDB allows us to use custom analyzers and control the analyzer per-field. If you're going to use the built-in Lucene analyzer for a field, you can need to pass the FullName of the analyzer type just like in the below example which is a straight copy and paste from the RavenDB documentation.

store.DatabaseCommands.PutIndex(
    "Movies",
    new IndexDefinition
        {
            Map = "from movie in docs.Movies select new { movie.Name, movie.Tagline }",
            Analyzers =
                {
                    { "Name", typeof(SimpleAnalyzer).FullName },
                    { "Tagline", typeof(StopAnalyzer).FullName },
                }
        });

On the other hand, RavenDB also allows us to drop our own custom analyzers in:

"You can also create your own custom analyzer, compile it to a dll and drop it in in directory called "Analyzers" under the RavenDB base directory. Afterward, you can then use the fully qualified type name of your custom analyzer as the analyzer for a particular field."

There are couple things that you need to be careful of when going down this road:

After I stopped my RavenDB server, I dropped my assembly, which contains the TurkishLowerCaseKeywordAnalyzer, into the "Analyzers" folder under the RavenDB base directory. At the client side, here is my code which consists of the index creation and the query:

class Program
{
    static void Main(string[] args)
    {
        const string DefaultDatabase = "EqualsTryOut";
        IDocumentStore store = new DocumentStore
        {
            Url = "http://localhost:8080",
            DefaultDatabase = DefaultDatabase
        }.Initialize();

        IndexCreation.CreateIndexes(typeof(Users).Assembly, store);
        store.DatabaseCommands.EnsureDatabaseExists(DefaultDatabase);

        using (var ses = store.OpenSession())
        {
            var user = new User { 
                Name = "Irmak", 
                Roles = new List<string> { "adMin", "GuEst" } 
            };
            ses.Store(user);
            ses.SaveChanges();

            //this finds name:Irmak
            var user1 = ses.Query<User, Users>()
                .Where(usr => usr.Name == "irmak")
                .FirstOrDefault();
        }
    }
}

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
    public ICollection<string> Roles { get; set; }
}

public class Users : AbstractIndexCreationTask<User>
{
    public Users()
    {
        Map = users => from user in users
                       select new 
                       {
                          user.Name 
                       };

        Analyzers.Add(
            x => x.Name, 
            typeof(LuceneAnalyzers.TurkishLowerCaseKeywordAnalyzer)
                .AssemblyQualifiedName);
    }
}

It worked like a charm. I'm hopping this helps you solve this annoying problem and please post your comment if you know a better way of handling this.

Resources

Tags