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.