Archiv für den Monat Juni 2013

MVC Custom Validation mit IValidatableObject Interface


Letzte Woche wurde ich auf ein Interface aufmerksam gemacht, welches ich bisher in MVC noch nicht benutzt habe. Dabei handelt es sich um das IValidatableObject Interface, mit diesem Interface kann man sehr einfach alle möglichen und komplexen Validierungen für sein Model einbinden. Die Daten können dann an geeigneter Stelle mit “ModelState.IsValid” geprüft werden.

Dafür muss im Model nur das Interface “IValidatableObject” eingebunden werden und mit dem Interface noch die dazu verbundene Funktion “Validate”. In dieser Funktion werden die Daten nach den eigenen Vorstellungen geprüft, nach Abfragen die die Standardattribute wie “Required”, “MinLength”, …nicht abdecken. Dazu gehört z.B. der Vergleich zweier Datumswerte oder Strings. Dazu lassen sich dann Benutzerdefinierte Fehlermeldungen angeben und bei welchen Eingabefeldern diese Fehlermeldungen angezeigt werden sollen.

public class CustomValidationModel : IValidatableObject
{
    [Required]
    public string Name { get; set; }

    [Required]
    [DataType(DataType.Date)]
    public DateTime Birthdate { get; set; }

    [Required]
    [DataType(DataType.Date)]
    public DateTime Deathdate { get; set; }

    [Required]
    public int MinValue { get; set; }

    [Required]
    public int MaxValue { get; set; }

    public void Initilize()
    {
        Birthdate = DateTime.Now.Date;
        Deathdate = DateTime.Now.Date.AddYears(100);
    }

    /// 
    /// Die Custom Validate Methode die durch das Interface eingebunden wird.
    /// 
    public IEnumerable Validate(ValidationContext validationContext)
    {
        if (Birthdate.Date < DateTime.Now)
        {
            yield return new ValidationResult("Das Geburtsdatum muss in der Zukunft liegen", new[] { "Birthdate" });
        }

        if (MinValue  10 && MinValue > 10)
        {
            //Ausgeben der passenden Fehlermeldung und angeben bei welchen Werten die Fehlermeldung angezeigt werden soll
            yield return new ValidationResult("MaxValue und MinValue dürfen nicht größer wie 10 sein.", new[] { "MinValue", "MaxValue" });
        }

        if (Name.Trim().Length > 3 && !Name.StartsWith("Sq"))
        {
            yield return new ValidationResult("Wenn der Name länger wie 3 Zeichen ist, muss dieser mit 'Sq' beginnen.", new[] { "Name" });
        }
    }
}

Dazu muss im Controller nur noch “ModelState.IsValid” geprüft werden und schon haben wir unsere eigene Validierung eingebunden.

public ActionResult CustomValidation()
{
    CustomValidationModel model = new CustomValidationModel();
    model.Initilize();
    return View("CustomValidation", model);
}

[HttpPost]
public ActionResult CustomValidation(CustomValidationModel model)
{
    if (ModelState.IsValid)
    {
        //TODO Valides Model - Aufgaben ausführen
    }

    return View("CustomValidation", model);
}

Im View selbst können die Fehlermeldungen wie gewohnt direkt über “Html.ValidationMessageFor()” bei jedem Eingabefeld ausgegeben werden oder über das “Html.ValidationSummary()” global angezeigt werden.

</pre>
<div class="control-group">@Html.LabelFor(model => model.Name, new { @class = "control-label" })
<div class="controls">@Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name)</div>
</div>
<pre>

 

Der Nachteil an dieser Lösung ist, das leider keine direkte Möglichkeit besteht die Serverseitige Validierung mit der Clientseitigen Validierung zu verbinden. Wer also Serverseitige und Clientseitige Validierung in MVC mit Bordmitteln verbinden möchte, der sollte sich zum ValidationAttribute und dem zugehörigen Interface IClientValidatable z.B. hier belesen.

Codeplex:

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier findet Ihr dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/CustomValidation”, “Models/HomeModels/CustomValidationModel”.

Advertisements

MVC 4 Web API Custom Authorize Attribut


Wie bereits in meinem letzten Artikel erwähnt, benötigt man für Web API ein eigenes Authorize Attribut aus einem anderen .NET Namespace. Das hier verwendete Authorize Attribut kommt aus dem Namespace “System.Web.Http.AuthorizeAttribute”.

Auch hier bietet Microsoft wieder eine Standardlösung an, die nur prüft ob der User eingeloggt ist, dafür muss wie bisher gewohnt im Web API Controller das Authorize Attribut aus dem oben genannten Namespace verwendet werden.

Gibt man mit seinen Web API Aufrufen aber Nutzerspezifische Daten zurück, die nicht jeder eingeloggte User abrufen darf, wird hier wieder ein eigenes Authorize Attribut benötigt. Hier handelt es sich um den gleichen Aufbau wie beim normalen Authorize Attribut. Man kann wieder auf den Controler, Action, Id und sonstige Routing Informationen zugreifen und dann entsprechend im Custom Authorize Attribut darauf reagieren. Damit man weiß welcher User eingeloggt ist muss auch hier wieder das “FormsAuthentication.SetAuthCookie” beim normalen Seitenlogin benutzt werden.

Ich möchte zusätzlich noch darauf hinweisen, das ich für meine Web API Aufrufe nicht die Standardroute verwende. Damit wir die Web API auch so nutzen können wie wir es bisher von den MVC Controllern gewohnt sind, müssen wir noch eine weitere Route zu unserer WebApi hinzufügen. Denn mit der Standardroute für API aufrufe sind nur GET/POST/DELETE Abfragen möglich. Aber meist hat man mehr wie nur diese “paar” Abfragen die man für einen View benötigt, daher habe ich noch eine neue Route vor der “DefaultApi” Route hinzugefügt.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Registrieren unseres Filters für die Modelvalidation für AJAX Requests
        config.Filters.Add(new ValidateAttribute());

        //Alternatives Routing, hier ist es jetzt wieder möglich gezielte Actionen und Controller abzufragen
        //In der DefaultApi kann man nur Get/Post/Delete abfragen und ist damit sehr begrenzt was die Abfragen angeht.
        config.Routes.MapHttpRoute(
            name: "AlternateWebApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        //Standardroute  - hier lässt sich nur GET/POST/DELETE abfragen.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Unser Benutzerdefiniertes Web API Authorize Attribut sieht dann folgendermaßen aus und hier darauf achten das man das richtige Authorize Attribut aus dem richtigen Namespace einbindet!

using AuthorizeAttribute = System.Web.Http.AuthorizeAttribute;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomWebApiAuthorizeAttribute : AuthorizeAttribute
{
    #region Member
    private HttpActionContext CurrentContext { get; set; }
    #endregion

    /// <summary>
    /// Prüfen ob der User auch Autorisiert ist, auf die passenden API Aufrufe zuzugreifen
    /// </summary>
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        //Wenn der User nicht eingeloggt ist, dann wird er hier schon mit einem 401 abgewiesen
        base.OnAuthorization(actionContext);

        //Der Filtercontext wird benötigt um auf die RequestDaten zuzugreifen, z.b. auf die UserId die zugegriffen
        CurrentContext = actionContext;
       

        //Wenn kein User eingeloggt ist, dann hat er auch keinen Zugriff.
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            //Wenn der User nicht eingeloggt ist, auf die Loginseite verweisen
            HandleUnauthorizedRequest(actionContext);
        }
        else
        {
            //Prüfen der passenden Rechte für die einzelnen Actions/Methoden
            //Wenn der User nicht für den View/Action authentifiziert ist, dann auf die Loginseite verweisen.
            if (!CheckRights())
            {
                HandleUnauthorizedRequest(actionContext);
            }
        }

        //Wenn alles i.o. ist "nichts" unternehmen und einfach beim Aufbau der Seite weitermachen.
    }

    /// <summary>
    /// Den unautorisierten Eingriff, "abwehren"
    /// </summary>
    /// <param name="actionContext"></param>
    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        throw new HttpResponseException(challengeMessage);
    }

    #region Private Functions
    /// <summary>
    /// Prüfen um welchen CurrentController es sich handelt und die passende Sicherheitsprüfung vornehmen
    /// Hier werden Nur API Controller überprüft
    /// </summary>
    /// <returns>TRUE->Darf zugreifen | FALSE->Darf nicht zugreifen</returns>
    private bool CheckRights()
    {
        //Setzen des aktuellen Actionnamen, der aufgerufen wird.
        string actionName = CurrentContext.ActionDescriptor.ActionName;
        //Laden des Controllernamens über den der API Aufruf kam
        string controllerName = CurrentContext.ControllerContext.ControllerDescriptor.ControllerName;
        //Auslesen der ID, wenn eine in der URL mit übergeben wurde.
        string id = CurrentContext.ControllerContext.RouteData.Values.ContainsKey("id") ?
                    CurrentContext.ControllerContext.RouteData.Values["id"].ToString() : null;

        //Den aktuellen User ermitteln der den API Aufruf gestartet hat.
        string username = HttpContext.Current.User.Identity.Name;

        //TODO Prüfung vornehmen ob der Aktuelle User auf die ermittelten Resourcen zugreifen darf.

        return true;
    }
    #endregion
}

Das Attribut kann dann wieder problemlos über dem passenden ApiController verwendet werden

[CustomWebApiAuthorize]
public class WorktimeApiController : ApiController
{
    /// <summary>
    /// Filtert unsere Arbeitszeiteinträge nach den letzten Einträgen die vorgenommen wurde.
    /// </summary>
    /// <param name="id">Die zugehörige UserId zu der die Einträge gefiltert werden sollen</param>
    /// <param name="value">Unsere Modeldaten nach denen gefiltert werden soll</param>
    public HttpResponseMessage ByEntryFilter(long id, WorktimeEntryFilterModel value)
    {
        if (value != null)
        {
            WorktimeListModel worktime = new WorktimeListModel(id, value);
            //Den passen Listentyp speichern, wenn der jeweilige Filter ausgewählt wurde.
            worktime.SaveDefaultListType(id, EWorktimeListTypes.ByEntries);
            worktime.FilterWorktimeList();

            //Wird automatisch in JSON Objekt umgewandelt und gibt den StatusCode 200 für OK zurück!
            HttpResponseMessage response = Request.CreateResponse<WorktimeListModel>(HttpStatusCode.OK, worktime);
            return response;
        }

        //Statuscode 400 Zurückgeben
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }

...

ASP.NET MVC 4 Custom Authorize Attribute


Wie bereits in einem älteren Artikel von mir beschrieben gibt es hier noch einmal eine kleine Auffrischung der Materie inklusive eines Beispiels auf Codeplex.

Wozu ist das Authorize Attribut eigentlich da? Das Authorize Attribut kümmert sich darum, das nur autorisierte (eingeloggte) User den “Internen” Bereich einer Webseite anschauen können, dabei kann es sich z.B. um den Administratorbereich handeln.

Microsoft stellt von Haus aus bereits ein “Authorize” Attribut zur Verfügung was aber standardmäßig nur prüft ob der User auch eingeloggt ist. Wichtig ist hier das beim Login “FormsAuthentication.SetAuthCookie” verwendet wird, wenn der Login erfolgreich war. Denn damit weiß dann auch das Standard “Authorize” Attribut ob der User auf die jeweilige Aktion/Controller zugreifen darf.

Wenn man das “Authorize” Attribut direkt auf den ganzen Controller anwendet, kann man einfach mit einem “AllowAnonymous” Attribut über der jeweiligen Action diese Action wieder “freigeben” und auch ein User der nicht eingeloggt ist kann dann auf diese Action zugreifen.

Im Folgenden Beispiel sieht man ein Beispiel mit dem Standard “Authorize” Attribut, welches auf den gesamten Controller angewendet wird. Damit sich dann ein User auch noch einloggen kann, muss die Login Funktion mit einem “AllowAnonymous” Attribut bestückt werden. Außerdem sieht man bei der Loginmethode die Verwendung von “FormsAuthentication.SetAuthCookie”.

[Authorize]
public class AccountController : Controller
{
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }

    [AllowAnonymous]
    [HttpPost]
    public ActionResult Login(LoginModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            //ACHTUNG HIER müsste vorher nat. ein DB Abgleich mit den Usern aus der DB stattfinden
            //Aktuell setzten wir hier jeden User als Authorisiert, wenn er einen beliebigen Usernamen und PW angibt.
            FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
            
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }

        }

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

    public ActionResult LogOff()
    {
        FormsAuthentication.SignOut();

        return RedirectToAction("Index", "Home");
    }

Dann gibt es noch die Möglichkeit ein eigenes Authorize Attribut zu erstellen in dem man von “AuthorizeAttribute” Attribut ableitet (System.Web.Mvc.AuthorizeAttribute).

Ein eigenes Authorize Attribut ist z.B. notwendig wenn man noch zusätzliche Rechte bei einigen Actions oder ganzen Controllern für einzelne User überprüfen muss. Eine Seite die einen eigenen Admin Bereich hat, benötigt eine Extra Prüfung der Rechte für den Admin Bereich, denn hier hat nicht jeder Autorisierte Nutzer auch Zugriff, sondern nur spezielle Nutzer denen das Recht über die eigene Rechteverwaltung zugewiesen wurde. Dafür gibt es dann das eigene Authorize Attribut.

Bei der hier gezeigten Variante setzt Ihr nur das Authorize Attribut auf den jeweiligen Controller oder Action und eine Prüfung welcher User bzw. welche Rolle auf diesen Controller Zugriff hat, wird alles im Authorize Attribut selbst erledigt. Denn dort können wir auch sehen welche Action, Controller, Id oder Custom Route Values gerade aufgerufen wurde und entsprechend eure Rechte prüfen.

Ich finde der Vorteil an dieser Umsetzung liegt vor allem darin das ich eine zentrale Stelle habe an der ich meine Rechte für Alle Controller und Actions verwalten und setzten kann. Ich bin mit dieser Variante bisher sehr gut zurecht gekommen.

using AuthorizeAttribute = System.Web.Mvc.AuthorizeAttribute;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    #region Member
    private AuthorizationContext CurrentContext { get; set; }
    #endregion

    #region Konstruktor
    /// <summary>
    /// Es muss nichts übergeben werden, da controller und action direkt hier ausgelesen werden können
    /// </summary>
    public CustomAuthorizeAttribute()
    {
    }
    #endregion

    #region Public Functions
    /// <summary>
    /// Funktion die ausgeführt wird, wenn das Attribut "genutzt" wird, das Authorisation Attribut wird immer als erstes Attribut ausgeführt.
    /// </summary>
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);
        //Der Filtercontext wird benötigt um auf die RequestDaten zuzugreifen, 
        //z.b. auf die Id, Action oder den Controller auf den zugegriffen werden soll.
        CurrentContext = filterContext;
      
        //Wenn kein User eingeloggt ist, dann hat er auch keinen Zugriff.
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            //Wenn der User nicht eingeloggt ist, auf die Loginseite verweisen
            filterContext.Result = new HttpUnauthorizedResult();
        }
        else
        {
            //Prüfen der passenden Rechte für die einzelnen Actions/Methoden 
            if (!CheckRights())
            {
                //Auf die Startseite verweisen, wenn der User keinen Zugriff auf den Kontent hat, den er angefordert hat.
                filterContext.Result = new HttpUnauthorizedResult();
                //Oder auf eine beliebige Andere Webseite verweisen, wenn man nicht unbefingt die Startseite anzeigen möchte.
                //filterContext.Result = new RedirectResult("~/Home/Index");
            }
        }

        //Wenn alles i.o. ist "nichts" unternehmen und einfach beim Aufbau der Seite weitermachen.
    }
    #endregion

    #region Private Functions
    protected void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        throw new HttpResponseException(challengeMessage);
    }

    /// <summary>
    /// Prüfen um welchen Controller es sich handelt und die passende Sicherheitsprüfung vornehmen
    /// </summary>
    /// <returns>TRUE->Darf zugreifen | FALSE->Darf nicht zugreifen</returns>
    private bool CheckRights()
    {
        //Wir können hier recht einfach ermitteln um welchen Controller oder View es sich handelt den wir gerade 
        //überprüfen wollen - wenn wir erst einmal von der Standardroute ausgehen.
        string controllerName = CurrentContext.RouteData.GetRequiredString("controller");
        string actionName = CurrentContext.RouteData.GetRequiredString("action");
        string id = CurrentContext.RouteData.Values.ContainsKey("id") ? CurrentContext.RouteData.Values["id"].ToString() : null;

        //Den aktuell eingeloggten User ermitteln für den die Rechte geprüft werden sollen 
        //Der wert der im Username steht legen wir beim Login fest
        //bei "FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe)" 
        //wird hier der Username oder auch die UserId angezeigt.
        string userName = HttpContext.Current.User.Identity.Name;
        

        //Prüfen welcher Controller das Authorize Attribut aufgerufen hat und dann schauen welche Action aufgerufen wurde.
        switch (controllerName)
        {
            case "Home":
                //Rechteprüfung für den jewiligen Controller und evtl. auch auf die 
                //jeweilige Action bezogen durchführen
                return CheckHomeRights();
                break;

            case "Person":
                break;

        }

        return false;
    }

    /// <summary>
    /// Prüft alle Rechte für den HomeController und den enthaltenen Actions
    /// </summary>
    /// <returns></returns>
    private  bool CheckHomeRights()
    {
        return false;
    }
    #endregion
}

Aufruf des Custom Authorize Attributes z.B. für einen kompletten Controller

 [CustomAuthorize]
 public class HomeController : Controller
 {
     public ActionResult Index()
     {
         ViewBag.Message = "Modify this ...";

         return View();
     }
...

}

Wenn man jedoch einen WebAPI Controller überprüfen möchte ist dafür wieder ein anderes Custom Authorize Attribut zuständig, das findet Ihr dann in meinem nächsten Blogeintrag.

Codeplex:

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier findet Ihr dann die Wichtigsten Dateien im Webprojekt unter “Views/Home”, “Helpers/Authentication” und “Views/Account”. Das Projekt sollte auch so lauffähig sein.

Deploymentfehler – allowDefinition= ‚MachineToApplication‘


Beim Automatischen Veröffentlichen (Publish) meines MVC Projektes bekam ich folgenden Fehler:

““Einen Abschnitt, der als allowDefinition=’MachineToApplication‘ registriert ist, über die Programmebene hinaus zu verwenden verursacht einen Fehler. Dieser Fehler kann von einem virtuellen Verzeichnis verursacht werden, das nicht als Anwendung in IIS konfiguriert ist.”

Dieser lies sich leicht beheben nach dem ich folgenden Eintrag aus meiner *.csproj Datei des Webprojektes entfernt habe:

“<MvcBuildViews>true</MvcBuildViews>”

Diesen Eintrag hatte ich meinem MVC Projekt selbst hinzugefügt um auch die MVC Views direkt beim Erstellen auf Fehler zu prüfen, siehe Blogeintrag.

Nach einem guten Hinweis von B. ABT habe ich noch einmal etwas Recherchiert und natürlich gibt es mehrere Möglichkeiten, das Problem zu beseitigen. Denn das Attribut “MvcBuildViews” ist beim Compilieren von MVC Projekten mehr als nur hilfreich.

Die zweite Möglichkeit die Fehlermeldung beim Publish und Build zu entfernen, wäre einfach ein Pre-build Event in eurem Webprojekt anzulegen, welches den Ordner “obj” vor jedem Erstellen entfernt.

Dazu einfach folgende Zeile im Pre-build eures Webprojektes einfügen:

rd  /S /Q „$(ProjectDir)\obj“

B. ABT hat mich zwar auch noch auf folgendes Tool hingewiesen: http://wf.codeplex.com/SourceControl/latest dies schien mir aber doch etwas “riskant” und evtl. überdimensioniert für mein Problem. Ist aber ein Cooles Tool wenn man mal schnell nur den Source Code und Projektdateien aus einem Projekt benötigt ohne TFS, Resharper Dateien, ….