Monday, April 20, 2015

Extract all MVC / WebApi area/controller/actions

I did some search and created my own based on the answers found in SO.

Extract all MVC / WebApi area/controller/actions - Gist

// http://stackoverflow.com/questions/5508050/how-to-get-a-property-value-based-on-the-name
// http://forums.asp.net/t/1600843.aspx?How+list+all+of+the+Actions+in+an+MVC+application+for+security+audit+
// http://stackoverflow.com/questions/5801630/mvc-get-all-action-methods
// http://stackoverflow.com/questions/21583278/getting-all-controllers-and-actions-names-in-c-sharp
// http://stackoverflow.com/questions/15690834/asp-net-mvc4-list-of-all-areas
// http://stackoverflow.com/questions/1091853/error-message-unable-to-load-one-or-more-of-the-requested-types-retrieve-the-l
 
/// <summary>
/// Extract all MVC/WebApi area/controller/actions
/// </summary>
public class ResourceHelper
{
private const string NamespacePrefix = "github";
 
public static List<ApplicationResource> GetResources()
{
var resources = new List<ApplicationResource>();
 
try
{
resources.AddRange(MapResources<IController>()); // MVC
resources.AddRange(MapResources<IHttpController>(true)); // Web API
}
catch (ReflectionTypeLoadException ex)
{
var sb = new StringBuilder();
foreach (Exception exSub in ex.LoaderExceptions)
{
sb.AppendLine(exSub.Message);
var exFileNotFound = exSub as FileNotFoundException;
if (exFileNotFound != null)
{
if (!string.IsNullOrEmpty(exFileNotFound.FusionLog))
{
sb.AppendLine("Fusion Log:");
sb.AppendLine(exFileNotFound.FusionLog);
}
}
sb.AppendLine();
}
string errorMessage = sb.ToString();
//Display or log the error based on your application.
TraceHelper.Error(errorMessage);
}
 
return resources;
}
 
private static IEnumerable<ApplicationResource> MapResources<T>(bool api = false)
{
var cSuffix = api ? "ApiController" : "Controller";
 
var assemblies = AppDomain.CurrentDomain.GetAssemblies(); // currently loaded assemblies
 
var areaRegistrations = GetAreaRegistrations(assemblies).ToList();
 
var controllerTypes = GetControllerTypes<T>(assemblies, cSuffix);
 
var controllerMethods = GetControllerActions(controllerTypes, api);
 
foreach (var controllerType in controllerMethods)
{
string area = GetAreaName(controllerType, areaRegistrations);
string controller = controllerType.Key.Name.Replace(cSuffix, "");
foreach (MethodInfo action in controllerType.Value)
{
string actionName = action.Name;
bool attrGet = action.GetCustomAttributes(false).Any(a => a.GetType().IsAssignableFrom(typeof(HttpPostAttribute)));
yield return new ApplicationResource(area, controller, actionName, attrGet, api);
}
}
}
 
private static string GetAreaName(
KeyValuePair<Type, IEnumerable<MethodInfo>> controllerType,
IEnumerable<Type> areaRegistrations)
{
if (controllerType.Key.Namespace != null && controllerType.Key.Namespace.Contains("Areas"))
{
var areaNamespace = controllerType.Key.Namespace;
var regArea = areaRegistrations.First(a => a.Namespace != null && areaNamespace.Contains(a.Namespace));
var areaNameProp = regArea.GetProperty("AreaName");
var instance = Activator.CreateInstance(regArea); // have to create an instance before retrieving the property value
var areaNameValue = areaNameProp.GetValue(instance);
return areaNameValue.ToString();
}
return string.Empty;
}
 
private static Dictionary<Type, IEnumerable<MethodInfo>> GetControllerActions(IEnumerable<Type> controllerTypes, bool api)
{
var controllerMethods = controllerTypes.ToDictionary(
controllerType => controllerType,
controllerType => controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
!m.IsDefined(typeof(NonActionAttribute))
// NOTE : We are using 'HttpResponseMessage' as the return in every API action method.
// if that isn't the case with your API,
// something like this will get you to a close result :
// ==> (api || typeof(ActionResult).IsAssignableFrom(m.ReturnType))
&& (api ? typeof(HttpResponseMessage).IsAssignableFrom(m.ReturnType) : typeof(ActionResult).IsAssignableFrom(m.ReturnType))
&& m.Name != "Dispose"
&& !m.IsSpecialName
&& !m.IsStatic));
return controllerMethods;
}
 
private static IEnumerable<Type> GetControllerTypes<T>(IEnumerable<Assembly> assemblies, string controllerSuffix = "Controller")
{
var controllerTypes = assemblies
.SelectMany(a => a.GetTypes())
.Where(t => t != null
&& t.IsPublic // public controllers only
&& t.Name.EndsWith(controllerSuffix, StringComparison.OrdinalIgnoreCase) // enfore naming convention
&& t.Namespace.IfNotNull(n => n.ToLowerInvariant().StartsWith(NamespacePrefix))
&& !t.IsAbstract // no abstract controllers
&& typeof(T).IsAssignableFrom(t));
// should implement T (happens automatically when you extend Controller/ApiController)
return controllerTypes;
}
 
private static IEnumerable<Type> GetAreaRegistrations(IEnumerable<Assembly> assemblies)
{
var areaRegistrations = assemblies.SelectMany(a => a.GetTypes())
.Where(t => t != null && typeof(AreaRegistration).IsAssignableFrom(t));
return areaRegistrations;
}
 
}
 
public class ApplicationResource
{
public ApplicationResource(string area, string controller, string action, bool post, bool api)
{
Area = area;
Controller = controller;
Action = action;
IsGet = !post;
IsApi = api;
}
 
public string Area { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public bool IsGet { get; set; }
public bool IsApi { get; set; }
}
Following class contains an extension method used in the helper.
public static class Extensions{
public static TInner IfNotNull<T, TInner>(this T source, Func<T, TInner> selector, bool createNew = false)
where T : class
{
return source != null
? selector(source) : (createNew ? Activator.CreateInstance<TInner>() : default(TInner));
}
}

Sunday, April 19, 2015

Bing-Mail Easy Post Api - Version 1.3 - C#

Recently I had a chance to integrate an awesome and innovative postal mailing API (http://www.bingmail.com.au/) in my system. However I ran into some problems because their code samples were in PHP and doing the same from .NET was giving me errors. Finally I found a solution and may be someone out there will benefit from it.

The most frustrating error I had to fix was uploading files as a PUT HTTP method with Digest Authentication.

Complete solution except some easy-to-figure-out methods are posted in this gist. Bing-Mail Easy Post Api - Version 1.3

What i did was modified the DigestAuthFixer from http://stackoverflow.com/a/3117042/959245 to support any HTTP method.

Then used that to create the session, when we create the session using DigestAuthFixer it stores the Digest-Auth headers which i can reuse when uploading the files.
 
 
 using (var client = new WebClient())
{
    var uri = new Uri(_easypostHosts[2] + UploadUri.FormatWith(sessionId, HttpUtility.UrlEncode(fileName)));

    // get the auth headers which are already stored when we create the session
    var digestHeader = DigestAuthFixer.GetDigestHeader(uri.PathAndQuery, "PUT");
    // add the auth header to our web client
    client.Headers.Add("Authorization", digestHeader);

    // trying to use the UploadFile() method doesn't work in this case. so we get the bytes and upload data directly 
    byte[] fileBytes = File.ReadAllBytes(filePath);

    // as a PUT request
    var result = client.UploadData(uri, "PUT", fileBytes);

    // result is also a byte[].
    content = result.Length.ToString();
} 

Thursday, April 9, 2015

Upgrading to ASP.NET Identity 2 - Then Fix Errors

I am working on a MVC5 project but a lot of libraries it has referenced were outdated. Therefore I just opened up "Managed Nuget Pkgs For Solution" and clicked on update all button. It did update all and then I was so happy, i built the project, and ran it.

Lucky Me! I got these errors. Sounds familiar?

The model backing the 'ApplicationDbContext' context has changed since the database was created. This could have happened because the model used by ASP.NET Identity Framework has changed or the model being used in your application has changed. To resolve this issue, you need to update your database. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=301867).  Before you update your database using Code First Migrations, please disable the schema consistency check for ASP.NET Identity by setting throwIfV1Schema = false in the constructor of your ApplicationDbContext in your application.       public ApplicationDbContext() : base("ApplicationServices", throwIfV1Schema:false)

---------------------- fixed the above, got the below error

One or more validation errors were detected during model generation:
Oklo.SmartTravel.Web.Models.IdentityUserRole: : EntityType 'IdentityUserRole' has no key defined. Define the key for this EntityType.Oklo.SmartTravel.Web.Models.IdentityUserLogin: : EntityType 'IdentityUserLogin' has no key defined. Define the key for this EntityType.IdentityUserRoles: EntityType: EntitySet 'IdentityUserRoles' is based on type 'IdentityUserRole' that has no keys defined.IdentityUserLogins: EntityType: EntitySet 'IdentityUserLogins' is based on type 'IdentityUserLogin' that has no keys defined.

------------------------- fixed the above, got the below error

Invalid column name 'UserId'

----------------------------- fixed the above, it is working now

Then I read a lot of SO threads, tried different solutions, somehow it started working.

I can't use migrations on the database im using so any changes I do had to come from my code. I have no permission to change any of the database tables.

Solutions to these issues:

1. Goto Global.asax, add this line of code to Application_Start()

 Database.SetInitializer<ApplicationDbContext>(null);

"ApplicationDbContext" is the name of your database context class.

2. then change the ApplicationDbContext like the following code

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base(EnvironmentProfile.Current.GetValue("Security", "connectionString"), throwIfV1Schema: false)
        {
        }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            var userClaims = modelBuilder.Entity<IdentityUserClaim>()
                .ToTable("AspNetUserClaims");
            userClaims.Property(u => u.UserId).HasColumnName("User_Id");
        }
    }

What this code does is, mapping our new Identity2 'UserId' property to the AspNetUserClaims tables' User_Id column.

It should work now.

Just in case, If it does't, depending on the error message. If it says invalid column name, the all you need to do is check the column names in database and your classes. If there is a mismatch, fix it like I did with User_Id column.

If it says there is a table missing, or invalid table or table not found, then  a quick google search should land you on the table structure. Add that to your database, if any errors are popping up, fix them in the OnModelCreating method.

TIP:
You can create a default .NET project (MVC) which has Identity 2 enabled by default. Then, when you run it, your database will be auto generated, without any errors. Now that you have a working db which works with ID2, bugs and errors fixing is easier.