Dealing with Direct Object References in Web Api and Angular

I blogged here a long time ago about one way to deal with direct object references in ASP.NET MVC application.  Today I want to describe my solution to the same problem in an Angular application that is using ASP.NET Web Api for server data access.

Core approach is still the same, and I will still use the same encryption utility.  Since I do not have access to a specific controller type in this case, you have to decide what password you are going to use.  I am going to omit my process of picking a valid password.

Here is the outline of my solution

  • When I show the data on the search screen, my results are going to contain encrypted ids.
  • When an item is selected, I am going to pass this id to details Angular controller.
  • The controller will pass this id to the Web Api controller
  • I am going to inject a model binder that will dynamically decrypt the ID before Web Api controller gets it.
  • I will use a helper method inside my mapping API, based on AutoMapper that maps my Entity Framework objects to business objects, that will automatically encrypt primary key (integer).  To assist in this task I will add an interface to business objects that need this functionality

Let’s get started.

First of all, in the app I am working on I have separate business layer (custom objects) and database layer(Entity Framework Code first objects).  In most cases they are similar, and I use AutoMapper to map entity framework objects to returned business objects.  I have a helper class – MappingHelper that does the work.  In Map method it just calls AutoMapper Api.  I like the existence of the helper class because it allows me to inject extra code before and after the mappings.  In this case I am going to make it aware of the new interfaces:

    public interface IHaveEncryptedPrimaryKey : IHavePrimaryKey
    {
        string EncryptedId { get; set; }
    }
    public interface IHavePrimaryKey
    {
        int PrimaryKey { get; }
    }

Now, my mapping helper can take advantage of these in a simple fashion:

        public static TDestination Map<TDestination>(object source)
        {
            var returnValue = Mapper.Map<TDestination>(source);
            var itemWithEncryptedKey = returnValue as IHaveEncryptedPrimaryKey;
            if (itemWithEncryptedKey != null)
            {
                itemWithEncryptedKey.EncryptedId = EncryptionUtility.Encrypt(
                    itemWithEncryptedKey.PrimaryKey.ToString(CultureInfo.InvariantCulture), "your password here", true);
            }
            return returnValue;
        }

Very elegant, and I do not have to pepper many classes with the encryption code, just implement the interface on those classes that need it.  Here is an example:

    public class Role : IHaveEncryptedPrimaryKey
    {
        public Role()
        {
            Name = string.Empty;
            Description = string.Empty;
        }
        public int RoleId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }

        [JsonIgnore]
        public int PrimaryKey { get { return RoleId; }}


        public string EncryptedId { get; set; }
    }

I am using JsonIgnore not to output the primary key column, the rest is obvious. So, now I can dynamically encrypt primary keys as I am mapping database objects to business objects.

The code in Angular controller does not change much.  I simply need to pass encrypted id property instead of real ID from “search” Angular controller to “edit” Angular controller.  Something similar to the following (in TypeScript)

            $scope.editRole = (role: IRoleInfo) => {
                $location.path("/roles/edit/" + role.EncryptedId);
            };

Finally, when I am inside “edit” controller, I need to call Web Api controller, similar to the following:

  roleService.getRole(id, (result: IRole) => {
                    $scope.model = result;
                });

The last part is transparently handle the decryption of the ID before it is passed to the controller.  I will use model binders here, specifically I will write a class that implements IModelBinder.  The code is very simple.  If I expect an integer, but see something else, I will assume it is an encrypted ID.

    public class EncryptedIdModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            var input = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            if (input != null && !string.IsNullOrEmpty(input.AttemptedValue))
            {
                if (bindingContext.ModelType == typeof(int))
                {
                    int id;
                    if (!int.TryParse(input.AttemptedValue, out id))
                    {
                        var decrypted =
                            EncryptionUtility.Decrypt(input.AttemptedValue, "your password here", true);
                        if (int.TryParse(decrypted, out id))
                        {
                            bindingContext.Model = id;
                        }
                        else
                        {
                           bindingContext.Model = int.MinValue;
                        }
                    }
                    else
                    {
                        bindingContext.Model = id;
                    }
                }
            }

            return true;
        }
    }

 

Now, my Web Api controller method does not change!  It will still have a parameter called “id” that is an integer. 

        [Route("find"), HttpGet]
        public ExecutionResult<Role> Find(int id)
        {
            return Execute(() =>
            {
                var role = MappingHelper.Map<Role>(Repository<IRoleRepository>().GetRole(id));
                return role;
            });

        }

The last step is to add my new binder to global configuration.

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.BindParameter(typeof(int), new EncryptedIdModelBinder());
        }
    }

That is all there is to it.

Enjoy.