Introduction

In the last article we saw how a custom membership provider could be implemented so as to make effective use of the forms authentication model. Now let us extend this application to a task manager application that uses the authentication data to distinguish the users and perform activities related to that particular user. Remember that you have to modify the "AppDb" connection string in the sample project to point to your database.  

 part 1 of the article is here 

Extending The Application 

at this point, we have an elementary app. that authenticates using our custom membership provider class. Now, let us extend this to an app that could be used by users to enter tasks they want to remember. its per user and so we need to have a way by which we can identify the current user. this information would be required while adding tasks and listing tasks. to do this, firstly we need to create a forms authentication ticket as shown below. this would be in the "Register" action in the "Account" controller. now add a method to the AccountController.cs file as shown below:   

private void SetupFormsAuthTicket(string userName, bool persistanceFlag)


{

        User user = new Models.User();

        FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,

                                                    userName,

                                                    DateTime.Now,

                                                    DateTime.Now.AddMinutes(30),

                                                    persistanceFlag,

                                                    user.GetUserIDByUsername(userName).ToString());



        string encTicket = FormsAuthentication.Encrypt(authTicket);

        this.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));

} 

Notice the line <b>user.GetUserIDByUserName(userName)</b> in the list of parameters passed to the constructor of the FormsAuthenticationTicket class. This is the "userData" property which could be used to store our custom data that could be used elsewhere. For your reference at this point i have added the user id of the current user in the forms authentication ticket. I have given below the signature of the FormsAuthenticationTicket class' constructor. 

public FormsAuthenticationTicket(int version, string name, DateTime issueDate, DateTime expiration, bool isPersistent, string userData);

Next step would be to modify the LogOn and Register methods to call this method, if the user was successfully logged on or registered. I have given below the modified versions of the LogOn and Register methods.  

    [HttpPost]
    public ActionResult LogOn(LogOnModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            if (MembershipService.ValidateUser(model.UserName, model.Password))
            {
                FormsService.SignIn(model.UserName, model.RememberMe);

                SetupFormsAuthTicket(model.UserName,model.RememberMe);

                if (!String.IsNullOrEmpty(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                else
                {
                    return RedirectToAction("Index", "Home");
                }
            }
            else
            {
                ModelState.AddModelError("", "The user name or password provided is incorrect.");
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }

    [HttpPost]
    public ActionResult Register(RegisterModel model)
    {
        if (ModelState.IsValid)
        {
            // Attempt to register the user
            MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);

            if (createStatus == MembershipCreateStatus.Success)
            {
                FormsService.SignIn(model.UserName, false);

                SetupFormsAuthTicket(model.UserName,false);
                    
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
            }
        }

        // If we got this far, something failed, redisplay form
        ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
        return View(model);
    }
		 

What Next? 

Now, we have the forms authentication ticket with the user id of the current user. We have got to add a way for the views to consume this. For this purpose let us create a class that holds this ticket and the information that would be required by various pages. Lets call this as "UserIdentity". It has to implement the IIdentity interface. The purpose of implementing this interface would be clear in the following steps. Notice the "UserId" property which returns the "UserData" of the ticket that contains the user id of the currently logged on user. 

    public class UserIdentity : IIdentity
    {
        private System.Web.Security.FormsAuthenticationTicket ticket;

        public UserIdentity(System.Web.Security.FormsAuthenticationTicket _ticket)
        {
            ticket = _ticket;
        }

        public string AuthenticationType
        {
            get { return "DevUser"; }
        }

        public bool IsAuthenticated
        {
            get { return true; }
        }

        public string Name
        {
            get { return ticket.Name; }
        }

        public string UserId
        {
            get { return ticket.UserData; }
        }
    } 

Wrapping It Up  

Now, in order to let all the pages use this information, we need to set this information to the HttpContext.Current.User object. this is done in the Global.ascx file as shown below. Add an OnInit method to the file that attaches a PostAuthenticateRequest event. In the method called during the PostAuthenticateRequest event, notice that the authentication ticket is decrypted, an instance of  UserIdentity object is created and passed on to GenericPrincipal's constructor to create a GenericPrincipal object. this is the reason why UserIdentity had to implement the IIdentity interface. finally the principal object is assigned to the HttpContext.Current.User object.  
    public override void Init()
    {
        this.PostAuthenticateRequest += new EventHandler(MvcApplication_PostAuthenticateRequest);
        base.Init();
    }

    void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
    {
        HttpCookie authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
        if (authCookie != null)
        {
            string encTicket = authCookie.Value;
            if (!String.IsNullOrEmpty(encTicket))
            {
                FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(encTicket);
                UserIdentity id = new UserIdentity(ticket);
                GenericPrincipal prin = new GenericPrincipal(id, null);
                HttpContext.Current.User = prin;
            }
        }
    }
      

Creating The Table(s) Required 

Now, we have the required information in the User object. Before seeing how we use it, let us create a table for holding the tasks that users can add/view. The structure of the table is quite simple. note that it has a foreign key constraint with the Users table created earlier for holding our users.  

    SET ANSI_NULLS ON
    GO

    SET QUOTED_IDENTIFIER ON
    GO

    SET ANSI_PADDING ON
    GO

    CREATE TABLE [dbo].[Tasks](
	    [TaskID] [int] IDENTITY(1,1) NOT NULL,
	    [TaskName] [varchar](50) NOT NULL,
	    [TaskDescription] [varchar](255) NOT NULL,
	    [TaskDateTime] [datetime] NOT NULL,
	    [UserID] [int] NOT NULL,
        CONSTRAINT [PK_Tasks] PRIMARY KEY CLUSTERED 
    (
	    [TaskID] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]

    GO

    SET ANSI_PADDING OFF
    GO

    ALTER TABLE [dbo].[Tasks]  WITH CHECK ADD  CONSTRAINT [FK_Tasks_Users] FOREIGN KEY([UserID])
    REFERENCES [dbo].[Users] ([UserID])
    GO

    ALTER TABLE [dbo].[Tasks] CHECK CONSTRAINT [FK_Tasks_Users]
    GO
    

The "Tasks" Repository       

Now that we have a table to represent a user's tasks, let us now create an object to hold the entries in this table. Given below is the TaskObj:   

    [Table(Name = "Tasks")]
    public class TaskObj
    {
        [Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
        public int TaskID { get; set; }
        [Column] public string TaskName { get; set; }
        [Column] public string TaskDescription { get; set; }
        [Column] public DateTime TaskDateTime { get; set; }
        [Column] public int UserID { get; set; }
    }
     

The next step would be to create the repository that would add a task or get a list of tasks by user id. It's given below:  

    public class Task
    {
        private Table<TaskObj> taskList;
        private DataContext context;

        public Task()
        {
            string connectionString = ConfigurationManager.ConnectionStrings["AppDb"].ConnectionString;
            context = new DataContext(connectionString);
            taskList = context.GetTable<TaskObj>();
        }

        public IEnumerable<TaskObj> GetTasksByUserID(int userID)
        {
            return taskList.Where(t => t.UserID == userID).OrderByDescending(t => t.TaskDateTime);
        }

        public bool AddTask(TaskObj task)
        {
            taskList.InsertOnSubmit(task);
            context.SubmitChanges();
            return true;
        }
    }    
    

Using The User Id Available In The Views

We come to the most interesting part (at least for this article!). How do we use the user related information we stored with the forms auth ticket? You are not far from seeing that! You just have to grab the User.Identity object from the System.Security.Principal.IPrincipal.Controller object. It's of type IIdentity that we saw earlier while constructing the UserIdentity type. So, we could safely cast it to UserIdentity.  

If you recollect UserIdentity type, we added a property called UserId which contains the user data we stored in the forms authentication ticket. This could be used in the controller for adding a task or listing the tasks associated to this user id. Check out the controller for adding a task below - see  how the user id is obtained. also, notice the Authorize attribute for the controller. Once we get the user id, i just call the tasks repository to add the task.    

    [Authorize]
    [HttpPost]
    public ActionResult AddTask(TaskObj task)
    {
        UserIdentity identity = (UserIdentity)User.Identity;
        Task _task = new Task();
        task.UserID = int.Parse(identity.UserId);
        task.TaskDateTime = DateTime.Now;
        _task.AddTask(task);

        return View(task);
    }
     
Now, let us just add a tab called "Task Manager", to the master page, so that we have a link through we can access the task manager. Open the Site.Master under the Views/Shared folder and add the following line:    
    <li>
    <%: Html.ActionLink("Task Manager", "Tasks", "Home") %>
    </li>
     

Now, add the views Tasks.aspx and AddTask.aspx under the Home folder. the Tasks view's model is strongly typed to an IEnumerable of TaskObj and simply lists the tasks using foreach. the controller for Tasks gets the list of tasks for the user by GetTasksByUserID method in the Tasks repository. it gets the user's id from the UserIdentity object as shown before in the AddTask controller. 

1.jpg

AddTask on the other hand, is strongly typed to TaskObj type and we use Html.BeginForm to display the user with a form and in the controller we call the AddTask method in the Tasks repository to add the task. you could have a look at the views in the project attached.   

2.jpg

Future Improvements Planned  

In future I plan to update this article to use dependency injection and I also plan to add ajax support to add/edit/delete tasks with a common management page instead of having separate  pages for add/edit/delete. 

Conclusion   

In my first article i explained how custom membership providers can be used for controlling how the users are authenticated in our application and in this article I have extended that application to do something more useful than just authenticate a user. I hope this article was helpful to you all! please feel free to post comments/questions in the comments section below. Again, thanks for reading my article!  

History            

First version released           

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架