MVC Error Handle with custom error Messages - c#

I'm building a new Web Application using MVC5 and I need the followings:
Catch errors
Log the details in a file
Send them by email
Add to the detail custom information (for example the Id of the
record I'm trying to read)
Return to the view custom messages to the user
I have found a lot of information regarding the HandleErrorAttribute but none of them allow to add specific details to the error, also I have found information saying that the try catch aproach is too heavy for the server.
For now, I have:
Controller:
public partial class HomeController : Controller
{
private static Logger logger = LogManager.GetCurrentClassLogger();
public virtual ActionResult Index()
{
try
{
return View();
}
catch (Exception e)
{
logger.Error("Error in Index: " + e);
return MVC.Error.Index("Error in Home Controller");
}
}
}
I have found this Extended HandleErrorAttribute that seems complete but don't do everything I need:
private bool IsAjax(ExceptionContext filterContext)
{
return filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
}
public override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
// if the request is AJAX return JSON else view.
if (IsAjax(filterContext))
{
//Because its a exception raised after ajax invocation
//Lets return Json
filterContext.Result = new JsonResult(){Data=filterContext.Exception.Message,
JsonRequestBehavior=JsonRequestBehavior.AllowGet};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
}
else
{
//Normal Exception
//So let it handle by its default ways.
base.OnException(filterContext);
}
// Write error logging code here if you wish.
//if want to get different of the request
//var currentController = (string)filterContext.RouteData.Values["controller"];
//var currentActionName = (string)filterContext.RouteData.Values["action"];
}

Your requirement best fit with Elmah. Very good plugin for logging errors.
ELMAH stands for Error Logging Modules And Handlers
ELMAH provides such a high degree of plugability that even Installation of ELMAH does not require compilation of your application.
ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable. It can be dynamically added to a running ASP.NET web application, or even all ASP.NET web applications on a machine, without any need for re-compilation or re-deployment.
Reference from the blog of SCOTT HANSELMAN
Just need to copy binary of ELMAH to bin folder of your application and edit web.config file. That's it!
you need to add following to your web.config and make some other changes described in the following links.
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
</sectionGroup>
For example to set up mail account.
<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
</sectionGroup>
</configSections>
<elmah>
<errorMail from="test#test.com" to="test#test.com"
subject="Application Exception" async="false"
smtpPort="25" smtpServer="***"
userName="***" password="***">
</errorMail>
</elmah>
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="CustomError.aspx">
<error statusCode="403" redirect="NotAuthorized.aspx" />
<!--<error statusCode="404" redirect="FileNotFound.htm" />-->
</customErrors>
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false"/>
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</httpHandlers>
<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
</httpModules>
</system.web>
</configuration>
Here is some good reference link (that contains detailed reference to installation of ELMAH to your project) for your reference.
https://msdn.microsoft.com/en-us/library/aa479332.aspx
https://code.google.com/p/elmah/wiki/MVC
Update
Add to the detail custom information (for example the Id of the record I'm trying to read)
You can build your own custom exception that derives from Exception.
public class MyException : Exception
{
public MyException(string message, Exception ex) : base(ex.Message, ex)
{
}
}
and then using it like
public virtual ActionResult Index()
{
try
{
return View();
}
catch (Exception e)
{
throw new MyException("detailed exception", e);
}
}
in this way main exception would be wrapped inside the myexception and you can add your detailed custom exception message.
Return to the view custom messages to the user
You just need to add
<system.web>
<customErrors mode="On">
</customErrors>
<sytem.web>
and add Error.cshtml inside the ~/View/Shared folder
Then whenever exception is encountered it will find for Error.cshtml inside view/shared folder and render the content. so you can render there your custom message.

Use Elmah as others have also recommended. I am, and haven't looked back!
It meets all your requirements:
Catches all errors, e.g. 400s, 500s...
Logs to a file, and any other data store you can think of, e.g. database, memory, Azure, more file formats(XML, CSV), RSS feed...
Emails errors: Enable and config mail settings in Web.config - very simple. You can even send emails asynchronously!
Add custom code - in your case add extra details to the error
Use your own custom error pages - custom error node (for 400s, 500s) in web.config and your own error controller
Further on the custom code (2nd last point above), AFAIK you have two options:
1. Create a custom error log implementation.
This isn't that difficult. It's what I did!
Override the default error log data store. For example, taking the SQL Server data store:
In Web.config
<elmah>
<errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="myCn" applicationName="myAppName" />
</elmah>
Next, create a class "MySQLServerErrorLog" and derive from Elmah.ErrorLog
All that is then required is to override the Log() method.
public override string Log(Error error)
{
// You have access to all the error details - joy!
= error.HostName,
= error.Type,
= error.Message,
= error.StatusCode
= error.User,
= error.Source,
// Call the base implementation
}
In Web.config, replace the default (above) entry with your implementation:
<elmah>
<errorLog type="myProjectName.MySQLServerErrorLog, myProjectName" />
</elmah>
2. You can programmatically log errors
Using the ErrorSignal class, you may logs errors without having to raise unhandled exceptions.
Syntax:
ErrorSignal.FromCurrentContext().Raise(new NotSupportedException());
Example: A custom exception
var customException = new Exception("My error", new NotSupportedException());
ErrorSignal.FromCurrentContext().Raise(customException);
This gives you the option of using your custom logic to programmatically log whatever you require.
I've written functionality for my Elmah instance to logs errors to Azure Cloud Storage Table and Blob (error stack trace details).
FWIW before I used Elmah, I had written my own exception handling mechanism for MVC which used HandleErrorAttribute and Application_Error (in Global.asax). It worked but was too unwieldy IMO.

If it was me, I'd create my own exception handling Attribute which adds required behaviour to the base implementation of HandleErrorAttribute.
I've had quite good results in the past with having attributes "pointed at" various parts of the request that's of interest (am thinking the bit where you say that you want to log specific details) - so you can use these identifiers to pull the request to pieces using reflection:
CustomHandleErrorAttribute(["id", "name", "model.lastUpdatedDate"])
I've used this approach to secure controller actions (making sure that a customer is requesting things that they're allowed to request) - e.g. a parent is requesting info on their children, and not someone else's children.
Or, you could have a configuration set up whereby you'd "chain" handlers together - so lots of little handlers, all doing very specific bits, all working on the same request and request pointers (as above):
ChainedErrorHandling("emailAndLogFile", ["id", "name", "model.lastUpdatedDate"])
Where "emailAndLogFile" creates a chain of error handlers that inherit from FilterAttribute, the last of which, in the chain, is the standard MVC HandleErrorAttribute.
But by far, the simplest approach would be the former of these 2.
HTH
EDITED TO ADD: Example of inheriting custom error handling:
public class CustomErrorAttribute : HandleErrorAttribute
{
public CustomErrorAttribute(string[] requestPropertiesToLog)
{
this.requestPropertiesToLog = requestPropertiesToLog;
}
public string[] requestPropertiesToLog { get; set; }
public override void OnException(ExceptionContext filterContext)
{
var requestDetails = this.GetPropertiesFromRequest(filterContext);
// do custom logging / handling
LogExceptionToEmail(requestDetails, filterContext);
LogExceptionToFile(requestDetails, filterContext);
LogExceptionToElseWhere(requestDetails, filterContext);// you get the idea
// even better - you could use DI (as you're in MVC at this point) to resolve the custom logging and log from there.
//var logger = DependencyResolver.Current.GetService<IMyCustomErrorLoggingHandler>();
// logger.HandleException(requestDetails, filterContext);
// then let the base error handling do it's thang.
base.OnException(filterContext);
}
private IEnumerable<KeyValuePair<string, string>> GetPropertiesFromRequest(ExceptionContext filterContext)
{
// in requestContext is the queryString, form, user, route data - cherry pick bits out using the this.requestPropertiesToLog and some simple mechanism you like
var requestContext = filterContext.RequestContext;
var qs = requestContext.HttpContext.Request.QueryString;
var form = requestContext.HttpContext.Request.Form;
var user = requestContext.HttpContext.User;
var routeDataOfActionThatThrew = requestContext.RouteData;
yield break;// just break as I'm not implementing it.
}
private void LogExceptionToEmail(IEnumerable<KeyValuePair<string, string>> requestDetails, ExceptionContext filterContext)
{
// send emails here
}
private void LogExceptionToFile(IEnumerable<KeyValuePair<string, string>> requestDetails, ExceptionContext filterContext)
{
// log to files
}
private void LogExceptionToElseWhere(IEnumerable<KeyValuePair<string, string>> requestDetails, ExceptionContext filterContext)
{
// send cash to me via paypal everytime you get an exception ;)
}
}
And On the controller action you'd add something like:
[CustomErrorAttribute(new[] { "formProperty1", "formProperty2" })]
public ActionResult Index(){
return View();
}

Firstly, you can define a filter attribute, and you can register it on startup in an MVC application in global.asax, so you can catch any kind of errors that occur while actions are invoking.
Note: Dependency Resolving is changeable. I'm using Castle Windsor for this story. You can resolve dependencies your own IOC container. For example, ILogger dependency. I used for this property injection while action invoking.
Windsor Action Invoker
For Example Filter:
public class ExceptionHandling : FilterAttribute, IExceptionFilter
{
public ILogger Logger { get; set; }
public void OnException(ExceptionContext filterContext)
{
Logger.Log("On Exception !", LogType.Debug, filterContext.Exception);
if (filterContext.Exception is UnauthorizedAccessException)
{
filterContext.Result = UnauthorizedAccessExceptionResult(filterContext);
}
else if (filterContext.Exception is BusinessException)
{
filterContext.Result = BusinessExceptionResult(filterContext);
}
else
{
// Unhandled Exception
Logger.Log("Unhandled Exception ", LogType.Error, filterContext.Exception);
filterContext.Result = UnhandledExceptionResult(filterContext);
}
}
}
This way you can catch everything.
So:
private static ActionResult UnauthorizedAccessExceptionResult(ExceptionContext filterContext)
{
// Send email, fire event, add error messages
// for example handle error messages
// You can seperate the behaviour by: if (filterContext.HttpContext.Request.IsAjaxRequest())
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
filterContext.Controller.TempData.Add(MessageType.Danger.ToString(), filterContext.Exception.Message);
// So you can show messages using with TempData["Key"] on your action or views
var lRoutes = new RouteValueDictionary(
new
{
action = filterContext.RouteData.Values["action"],
controller = filterContext.RouteData.Values["controller"]
});
return new RedirectToRouteResult(lRoutes);
}
In Global.asax:
protected void Application_Start()
{
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
}
FilterConfig:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ExceptionHandling());
}
BusinessException:
public class BusinessException : Exception, ISerializable
{
public BusinessException(string message)
: base(message)
{
// Add implemenation (if required)
}
}
So you can access the exception message OnException at ExceptionHandling class with filterContext.Exception.Message
You should use BusinessException on the action after any violated control logic this way: throw new BusinessException("Message").

Why don't you create model that contains your required Error Information and bind data to model when you need to? It will also allow you to create/return view from it

Global error catching with special information can you make with customer exceptions who contains the needed informations (id, tablesname etc.).
In HandleErrorAttribute you "only" have httpContext/ExceptionContext and othe static informations.

Related

WebApi Custom Authorize Attribute not working

I need to protect my web api with one or more specific users from the active directory, in the web.config I have the following code:
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
<section name="users" type="System.Configuration.NameValueFileSectionHandler,System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<users>
<add key="user" value="domain\loginname" />
</users>
<system.web>
<authentication mode="Windows" />
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
</system.web>
Then I have a custom authorize attribute which reads the user from the web.config section shown above.
public class MyAuthorizeAttribute : AuthorizeAttribute
{
public MyAuthorizeAttribute(params string[] userKeys)
{
List<string> users = new List<string>(userKeys.Length);
var allUsers = (NameValueCollection)ConfigurationManager.GetSection("users");
foreach (var userKey in userKeys)
{
users.Add(allUsers[userKey]);
}
this.Users = string.Join(",", users);
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool isAuthorized = base.AuthorizeCore(httpContext);
bool isRequestHeaderOk = false;
return isAuthorized && isRequestHeaderOk;
}
}
The problem is that the Authorize Core is never hit in the debugger, the JSON in the browser is always shown even if I put a hardcoded false , the breakpoint is never hit there.
Then I decorate my controllers with the custom authorize attribute
[MyAuthorize("user")]
[ResponseType(typeof(tblCargo))]
public IHttpActionResult GettblCargosByActivo()
{
var query = from c in db.tblCargos
orderby c.strCargo
select c;
//var result = Newtonsoft.Json.JsonConvert.SerializeObject(query);
//return result;
return Ok(query);
}
And in IIS, the only enabled method is Windows Authentication
when I browse to the site from another computer, then I get the authentication window, but the authoze method shown above is never hit.
THis is a nice post that lead me to the right direction (I believe)
Custom Authorization in Asp.net WebApi - what a mess?
You should use AuthorizeAttribute in System.Web.Http instead of System.Web.Mvc
Implement IsAuthorized instead.
protected override bool IsAuthorized(HttpActionContext actionContext)
{
bool isAuthorized = base.IsAuthorized(actionContext);
bool isRequestHeaderOk = false;
return isAuthorized && isRequestHeaderOk;
}
In my case, while refactoring, my controller class was no longer extending ApiController. This was necessary for my filter to fire.

Asp.net - Remove Folder Structure From My URL

Appologise if this has already been asked but I have an asp.net website and all my footer pages are stored in Visual Studio under
Views > Footer > [Page Names]
When i click on a footer link, my URL is displaying as:
http://www.mysite.co.uk/Views/Footer/testpage
What i'm after is removing the "/Views/Footer" from the URL so it loks like:
http://www.mysite.co.uk/testpage
I have no idea how to do this. Could someone please give me step by step guide on code to use and where to put it so that it does this.
when ever i try double clicking on my Global.asax file it automatically opens the Global.asax.cs file which i suspect is also wrong
Add reference to system.web.routing to project
add urlroutingmodule to http module in config:
<configuration>
...
<system.web>
...
<httpModules>
...
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</httpModules>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules>
...
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</modules>
<handlers>
...
<add name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</handlers>
...
</system.webServer>
</configuration>
Define routes in global.asax:
void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
void RegisterRoutes(RouteCollection routes)
{
// Register a route for Categories/All
routes.Add(
"All Categories",
new Route("Categories/All", new CategoryRouteHandler())
);
// Register a route for Categories/{CategoryName}
routes.Add(
"View Category",
new Route("Categories/{*CategoryName}", new CategoryRouteHandler())
);
// Register a route for Products/{ProductName}
routes.Add(
"View Product",
new Route("Products/{ProductName}", new ProductRouteHandler())
);
}
Create route handler classes
public class ProductRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
string productName = requestContext.RouteData.Values["ProductName"] as string;
if (string.IsNullOrEmpty(productName))
return Helpers.GetNotFoundHttpHandler();
else
{
// Get information about this product
NorthwindDataContext DataContext = new NorthwindDataContext();
Product product = DataContext.Products.Where(p => p.ProductName == productName).SingleOrDefault();
if (product == null)
return Helpers.GetNotFoundHttpHandler();
else
{
// Store the Product object in the Items collection
HttpContext.Current.Items["Product"] = product;
return BuildManager.CreateInstanceFromVirtualPath("~/ViewProduct.aspx", typeof(Page)) as Page;
}
}
}
}
create asp.net pages that process request:
protected void Page_Load(object sender, EventArgs e)
{
dvProductInfo.DataSource = new Product[] { Product };
dvProductInfo.DataBind();
}
protected Product Product
{
get
{
return HttpContext.Current.Items["Product"] as Product;
}
}
This is a good reference to work off, I have used this in the past on webforms apps and it worked like a charm.
If you're not using MVC, then you can implement an IHttpModule. There are several guides on the Internet on how to do this, such as Scott Guthrie's here: http://weblogs.asp.net/scottgu/tip-trick-url-rewriting-with-asp-net

HttpHandler for prefix in url

I am trying to convert http call in aspx to https
Back Ground : i have a Aspx page that is in https site.on that page i have reference to script of google
Aspx page reference :
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
i have created a HttpHandler for Prefix Http
IHttpHandler Interface implementation :
public class HttpToHttpsHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
try
{
context.Response.ContentType = "text/plain";
if (context.Request.RawUrl.Contains("http:"))
{
string newUrl = context.Request.RawUrl.Replace("http", "https");
context.Server.Transfer(newUrl);
}
}
catch (Exception)
{
throw;
}
}
}
Web.Config file registration code :
<httpHandlers>
<add verb="*" path="http:*" type="HttpToHttpsHandler , App_Code"/>
</httpHandlers>
But i am not getting the control in Http handler class.what could be the possible error.
I am wondering if your assembly is called "App_Code". In your type declaration you must enter the assembly name, not the folder name of the C# file.
The path attribute as far as I know it is relative position and it only can take one of two values: the name/file-name or the extension/file-extension to map. Like
<add verb="*" path="*.SampleFileExtension"
type="Example1 " />
Or
<add verb="*" path="demo.*"
type="Example1 " />
I tried a combination of both these and it also worked, which says that anything that starts with test and for any extension will be handler by handler:
<add verb="*" path="test*.*"
name="HelloWorldHandler"
type="demo.HelloWorldHandler,App_Code" />
But please notice that it is a relative path, so it means it does not include the http or https values from the URL and therefore a Handler cannot be used to validate URLs.
You need to define your assembly name which contains the HttpToHandler class.
The handler is defined as the class HttpToHttpsHandler in the your assembly which if is in the same project then it will be your application name.
Go through this article
<httpHandlers>
<add verb="*" path="*.aspx"
type="HttpToHttpsHandler , AssemblyName" />
</httpHandlers>
</system.web>
if (!Request.IsLocal && !Request.IsSecureConnection)
{
string redirectUrl = Request.Url.ToString().Replace("http:", "https:");
Response.Redirect(redirectUrl);
}
HttpRequest.IsSecureConnection Property determines whether the HTTP connection uses secure sockets ( HTTPS) or not .-MSDN

DNN 6 Module - How to leverage asynchronous calls

DotNetNuke 6 does not appear to support WebMethods due to modules being developed as user controls, not aspx pages.
What is the recommended way to route, call and return JSON from a DNN user module to a page containing that module?
It appears the best way to handle this problem is custom Httphandlers. I used the example found in Chris Hammonds Article for a baseline.
The general idea is that you need to create a custom HTTP handler:
<system.webServer>
<handlers>
<add name="DnnWebServicesGetHandler" verb="*" path="svc/*" type="Your.Namespace.Handler, YourAssembly" preCondition="integratedMode" />
</handlers>
</system.webServer>
You also need the legacy handler configuration:
<system.web>
<httpHandlers>
<add verb="*" path="svc/*" type="Your.Namespace.Handler, YourAssembly" />
</httpHandlers>
</system.web>
The handler itself is very simple. You use the request url and parameters to infer the necessary logic. In this case I used Json.Net to return JSON data to the client.
public class Handler: IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
//because we're coming into a URL that isn't being handled by DNN we need to figure out the PortalId
SetPortalId(context.Request);
HttpResponse response = context.Response;
response.ContentType = "application/json";
string localPath = context.Request.Url.LocalPath;
if (localPath.Contains("/svc/time"))
{
response.Write(JsonConvert.SerializeObject(DateTime.Now));
}
}
public bool IsReusable
{
get { return true; }
}
///<summary>
/// Set the portalid, taking the current request and locating which portal is being called based on this request.
/// </summary>
/// <param name="request">request</param>
private void SetPortalId(HttpRequest request)
{
string domainName = DotNetNuke.Common.Globals.GetDomainName(request, true);
string portalAlias = domainName.Substring(0, domainName.IndexOf("/svc"));
PortalAliasInfo pai = PortalSettings.GetPortalAliasInfo(portalAlias);
if (pai != null)
{
PortalId = pai.PortalID;
}
}
protected int PortalId { get; set; }
}
A call to http://mydnnsite/svc/time is properly handled and returns JSON containing the current time.
does anyone else have an issue of accessing session state/updating user information via this module? I got the request/response to work, and i can access DNN interface, however, when i try to get the current user, it returns null; thus making it impossible to verify access roles.
//Always returns an element with null parameters; not giving current user
var currentUser = UserController.Instance.GetCurrentUserInfo();

Administer asp.net website (create new users, assign users to roles, etc.) from a windows app

I have an asp.net web app that uses forms-based authentication, a SqlMembershipProvider (using an encrypted password format), and a SqlRoleProvider. I need to know if it's possible to administer the users (create new users, assign them to roles, etc.) from a windows application - the powers that be don't want any administrative functionality in the web app itself.
Here is the membership provider definition from web.config:
<membership defaultProvider="MyProvider">
<providers>
<add name="MyProvider"
type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="MyConnectionString"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="true"
applicationName="/MyWebApp"
requiresUniqueEmail="true"
passwordFormat="Encrypted"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="7"
minRequiredNonalphanumericCharacters="1"
passwordAttemptWindow="10"
passwordStrengthRegularExpression=""/>
</providers>
</membership>
And the role manager definition:
<roleManager enabled="true" defaultProvider="MyRoleManager">
<providers>
<add name="MyRoleManager"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="MyConnectionString"
applicationName="/MyWebApp" />
</providers>
</roleManager>
And here is the machineKey definition (necessary to be able to use encrypted passwords):
<machineKey
validationKey="BC50A82A6AF6A015C34C7946D29B817C00F04D2AB10BC2128D1E2433D0E365E426E57337CECAE9A0681A2C736B9779B42F75D60F09F142C60E9E0E8F9840DB46"
decryptionKey="122035576C5476DCD8F3611954C837CDA5FE33BCDBBF23F7"
validation="SHA1"
decryption="AES"/>
So, obviously, I have a Sql Server database that contains the users and roles for the web app. I'd like to create a separate windows app that references the web app assembly, and use the configured MembershipProvider, RoleProvider, and machineKey to create users, assign users to roles, etc. If that's not possible, I can duplicate the configuration settings from web.config within the windows app. But I don't know how to do this either.
Am I way out of line thinking that this is possible? I've tried googling for a solution, but the signal-to-noise ratio is really bad.
Some options:
You could use the Web Site
Administration Tool, which isn't
Windows-Forms-based, but isn't part
of your Web app, either. It comes
with Visual Studio and can be
accessed by clicking the ASP.NET
Configuration icon in the Solution
Explorer.
It's possible to directly manipulate
the provider database used by a
SqlMembershipProvider from a Windows
Forms app, but you might have to be
careful not to break things.
If you were to create a custom
membership provider, you'd be in
control of how membership and role
data is persisted. If you did that
you could create a reusable library
that could be used in the Web app and
a Windows Forms app, too.
I don't think trying to use a SqlMembershipProvider from a Windows Forms app is a practical approach.
I've come up with a solution, based on the other answers (who both got +1), and some other sites out there.
First, I created Application Config file (app.config). It mirrors exactly what is found in web.config from the web app, with the exception of how the connection string was handled:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />
</configSections>
<connectionStrings>
<add name="MyConnectionString"
connectionString ="SERVER=abc;UID=def;PWD=hij;Initial Catalog=klm;MultipleActiveResultsets=True"/>
</connectionStrings>
<system.web>
<membership defaultProvider="MySqlMembershipProvider">
<providers>
<add name="MySqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="MyConnectionString"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="true"
applicationName="/MyWebApp"
requiresUniqueEmail="true"
passwordFormat="Encrypted"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="7"
minRequiredNonalphanumericCharacters="1"
passwordAttemptWindow="10"
passwordStrengthRegularExpression=""/>
</providers>
</membership>
<roleManager enabled="true" defaultProvider="MySqlRoleManager">
<providers>
<add name="MySqlRoleManager"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="MyConnectionString"
applicationName="/MyWebApp" />
</providers>
</roleManager>
<machineKey
validationKey="BC50A82A6AF6A015C34C7946D29B817C00F04D2AB10BC2128D1E2433D0E365E426E57337CECAE9A0681A2C736B9779B42F75D60F09F142C60E9E0E8F9840DB46"
decryptionKey="122035576C5476DCD8F3611954C837CDA5FE33BCDBBF23F7"
validation="SHA1"
decryption="AES"/>
</system.web>
</configuration>
Then I created a helper class that provides access to two singletons: a MembershipProvider and a RoleProvider. This turned out to be easier than I thought, once I knew how to do it:
using System.Configuration;
using System.Reflection;
using System.Web.Security;
namespace WebAdminViaWindows
{
internal static class Provider
{
private static readonly string assemblyFilePath = Assembly.GetExecutingAssembly().Location;
static Provider()
{
Membership = CreateMembershipProvider();
Role = CreateRoleProvider();
}
public static MembershipProvider Membership { get; private set; }
public static RoleProvider Role { get; private set; }
private static MembershipProvider CreateMembershipProvider()
{
var config = ConfigurationManager.OpenExeConfiguration(assemblyFilePath);
var systemWebGroup = config.SectionGroups["system.web"];
if (systemWebGroup == null)
{
throw new ConfigurationErrorsException("system.web group not found in configuration");
}
var membershipSection = systemWebGroup.Sections["membership"];
if (membershipSection == null)
{
throw new ConfigurationErrorsException("membership section not found in system.web group");
}
var defaultProviderProperty = membershipSection.ElementInformation.Properties["defaultProvider"];
if (defaultProviderProperty == null)
{
throw new ConfigurationErrorsException("defaultProvider property not found in membership section");
}
var defaultProviderName = defaultProviderProperty.Value as string;
if (defaultProviderName == null)
{
throw new ConfigurationErrorsException("defaultProvider property is not a string value");
}
var providersProperty = membershipSection.ElementInformation.Properties["providers"];
if (providersProperty == null)
{
throw new ConfigurationErrorsException("providers property not found in membership section");
}
var providerCollection = providersProperty.Value as ProviderSettingsCollection;
if (providerCollection == null)
{
throw new ConfigurationErrorsException("providers property is not an instance of ProviderSettingsCollection");
}
ProviderSettings membershipProviderSettings = null;
foreach (ProviderSettings providerSetting in providerCollection)
{
if (providerSetting.Name == defaultProviderName)
{
membershipProviderSettings = providerSetting;
}
}
if (membershipProviderSettings == null)
{
if (providerCollection.Count > 0)
{
membershipProviderSettings = providerCollection[0];
}
else
{
throw new ConfigurationErrorsException("No providers found in configuration");
}
}
var provider = new SqlMembershipProvider();
provider.Initialize("MySqlMembershipProvider", membershipProviderSettings.Parameters);
return provider;
}
private static RoleProvider CreateRoleProvider()
{
var config = ConfigurationManager.OpenExeConfiguration(assemblyFilePath);
var systemWebGroup = config.SectionGroups["system.web"];
if (systemWebGroup == null)
{
throw new ConfigurationErrorsException("system.web group not found in configuration");
}
var roleManagerSection = systemWebGroup.Sections["roleManager"];
if (roleManagerSection == null)
{
throw new ConfigurationErrorsException("roleManager section not found in system.web group");
}
var defaultProviderProperty = roleManagerSection.ElementInformation.Properties["defaultProvider"];
if (defaultProviderProperty == null)
{
throw new ConfigurationErrorsException("defaultProvider property not found in roleManager section");
}
var defaultProviderName = defaultProviderProperty.Value as string;
if (defaultProviderName == null)
{
throw new ConfigurationErrorsException("defaultProvider property is not a string value");
}
var providersProperty = roleManagerSection.ElementInformation.Properties["providers"];
if (providersProperty == null)
{
throw new ConfigurationErrorsException("providers property not found in roleManagerSection section");
}
var providerCollection = providersProperty.Value as ProviderSettingsCollection;
if (providerCollection == null)
{
throw new ConfigurationErrorsException("providers property is not an instance of ProviderSettingsCollection");
}
ProviderSettings roleProviderSettings = null;
foreach (ProviderSettings providerSetting in providerCollection)
{
if (providerSetting.Name == defaultProviderName)
{
roleProviderSettings = providerSetting;
}
}
if (roleProviderSettings == null)
{
if (providerCollection.Count > 0)
{
roleProviderSettings = providerCollection[0];
}
else
{
throw new ConfigurationErrorsException("No providers found in configuration");
}
}
var provider = new SqlRoleProvider();
provider.Initialize("MySqlRoleManager", roleProviderSettings.Parameters);
return provider;
}
}
}
At this point all that's needed is to access the Membership and Role properties of the Provider class. As an example, the following prints out the first 10 users and their roles:
int total;
foreach (MembershipUser user in Provider.Membership.GetAllUsers(0, 10, out total))
{
var sb = new StringBuilder();
sb.AppendLine(user.UserName);
foreach (var role in Provider.Role.GetRolesForUser(user.UserName))
{
sb.AppendLine("\t" + role);
}
Console.WriteLine(sb.ToString());
}
I'm not sure what "best-practice" would be here, but a simple way that should work is this.
Make a new windows app
Add an Application Config file
(app.config)
Copy the appropriate settings into
the app.config (settings from above
^)
Add a reference to System.Web
And copy the code from your web app
that uses the above settings to
connect to the database
That should do what you want.

Categories