code it

Martins Tech Blog

GZip für WebAPI 2 aktivieren

Mit GZip-Komprimierung kann man einiges an Netzwerklast minimieren - insbesondere, wenn größere Datenmengen übertragen werden sollen, in denen häufig ähnliche Worte vorkommen. Auch wenn das meist genutzte JSON-Format nicht ganz so geschwätzig ist wie beispielsweise SOAP, so kommen doch auch hier beispielsweise die Eigenschaftsnamen in jedem Objekt wieder vor. Wenn man dann eine Liste von Objekten überträgt, so ergibt sich hier ein Einsparungspotenzial.

Im  Gegensatz zu Content-Negotiation, die die Web-API selbst übernimmt, gibt es offenbar keinen Automatismus für die Gzip-Komprimierung. Eine Suche ergab, verschiedene Lösungsansätze - z.B. den von Ben Foster oder den von Radenko Zec. Letzterer ist Vorlage für meine jetzige Lösung geworden.

Schritt 1 - Erzeugen eines ActionFilters, der die Response ändert
public class CompressionAttribute : ActionFilterAttribute
{
    public override async Task OnActionExecutedAsync(HttpActionExecutedContext context, CancellationToken cancellationToken)
    {
        var acceptEncoding = context.Request.Headers.AcceptEncoding;
        var acceptsGzip = acceptEncoding.Contains(new System.Net.Http.Headers.StringWithQualityHeaderValue("gzip"));

        if (!acceptsGzip)
        {
            return;
        }

        var content = context.Response.Content;
        if (content == null)
        {
            return;
        }

        var headers = context.Response.Content.Headers;
        var bytes = await content.ReadAsByteArrayAsync();

        var zlibbedContent = (await Compress(bytes)) ?? new byte[0];
        context.Response.Content = new ByteArrayContent(zlibbedContent);

        foreach (var header in headers)
        {
            if (header.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }
            context.Response.Content.Headers.Add(header.Key, header.Value);
        }
        context.Response.Content.Headers.Add("Content-Encoding", "gzip");
    }

    private static async Task Compress(byte[] value)
    {
        if (value == null)
        {
            return null;
        }

        using (var output = new MemoryStream())
        {
            using (var gzipStream = new GZipStream(output, CompressionMode.Compress, CompressionLevel.BestSpeed))
            {
                gzipStream.FlushMode = FlushType.Finish;
                await gzipStream.WriteAsync(value, 0, value.Length);
                   
            }
            return output.ToArray();
        }
    }
}
Um die Komprimierung selbst kümmert sich Ionic.Zlib

Die Implementierung ist an sich recht einfach. Zunächst wird geprüft, ob der Client Gzip-Encoding akzeptiert. Ist das nicht der Fall, dann verändert das Attribut die Ausgabe nicht. Anderenfalls wird die Ausgabe komprimiert und die Response neu aufgebaut. Durch die Neuzuweisung der Response werden auch alle bisherigen Content-Type-Header verworfen, weswegen diese im Nachgang wieder gesetzt werden müssen.

Im Anschluss wird dann dem Client noch mitgeteilt, dass es sich um komprimierten Inhalt handelt.

Schritt 2 - ActionFilter anwenden
Der ActionFilter kann nun verwendet werden, indem eine Web-API-Methode damit annotiert wird oder man aktiviert ihn global für alle Anfragen.
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ...

        config.Filters.Add(new CompressionAttribute());

        ...
    }
}
Das passiert in der WebApiConfig.

SSLPeerUnverifiedException in Verbindung mit Android und IIS8

Das Backend für meine App sollt nur über eine https-Verbindung erreichbar sein. Alles kein Problem dachte ich mir, schnell ein SSL-Zertifikat besorgt und dieses der https-Bindung der Webanwendung zugewiesen. Alles kein Problem und auch der Browser zeigt das kleine grüne Schloss an und der Zertifizierungspfad war ok.

Um so überraschter war ich, dass meine Android App nun jeden Request mit einer Exception quittierte: 
javax.net.ssl.SSLPeerUnverifiedException (SSL peer not authenticated)

Laut Dokumentation passiert das, wenn man ein selbst signiertes Zertifikat verwendet und die Tipps im Web sind sehr breit gestreut - unter anderem "stelle den HttpClient doch so ein, dass er allen Zertifikaten vertraut". Das hilft bei selbst signierten Zertifikaten für eine Demo sicher, für eine echte App mit Kundendaten verwende ich diesen Tipp lieber nicht.

Was war nun aber Ursache für das Problem? Ich hatte im IIS in der https-Bindung den Haken bei "Require Server Name Indication (SNI)" gesetzt. Diese Einstellung sorgt eigentlich dafür, dass sich mehrere verschlüsselt abrufbare Websites unterschiedlicher Domains einen Server mit nur einer IP Adresse teilen können. Allerdings können ältere Browser damit Probleme haben. Offenbar zählt der HttpClient in Android / Java dazu, denn kaum hatte ich den Haken entfernt, ließ sich problemlos auf meine REST-API über HTTPS zugreifen.

Update 14.08.2015
Nach einiger Recherche habe ich nun herausgefunden, dass die neueren HttpClients durchaus in der Lage sind, mit SNI umzugehen. In der App in der das Problem auftrat hatte ich Spring for Android in der Version 1 verwendet. Der HttpClient dort hat das Problem mit SNI. Die neuere Version 2 von Spring for Android verwendet einen HttpClient, der problemlos damit umgehen kann.

Facebook zeigt falschen Vorschau-Content an

In der letzten Woche war mir aufgefallen, dass bei einer meiner Seiten der falsche Inhalt in der Vorschau angezeigt wurde. Das Problem hatte mehrere Ursachen.

1. IPv6
Das erste Problem ließ sich recht einfach lösen: Ich hatte in der Domain-Konfiguration noch einen AAAA-Record auf die IPv6  des Hosters gesetzt. Offenbar versucht Facebook einen Zugriff über IPv6 durchzuführen. Damit kam der Inhalt der Vorgabeseite des Hosters und damit auch ein hübsches rotes Verkehrszeichen als Bild, was mit meiner Webseite nun so überhaupt nichts zu tun hat.

2. Cache
Nun wurde es aber spannend. Die IP-Zuordnung war korrekt und trotzdem wurde noch der alte Inhalt angezeigt. Problem 2 ist, dass Facebook die Inhalte cacht. Das ist natürlich nur dann ein Problem, wenn man die Inhalte ändert. Glücklicherweise gibt es von Facebook ein Tool (das eigentlich einen anderen Zweck hat), aber hier sehr hilfreich ist - der Open Graph Object Debugger. Hier kann man die Url eingeben und sich ansehen, was Facebook daraus ausliest zum Erzeugen der Vorschau. Und man kann hier auch erzwingen, dass die Daten neu abgerufen werden.

3. Bilder
Letztes Problem bei mir waren Bilder. Auf vielen Webseiten ist es gerade üblich, im Header einen Slider mit Bildern zu haben. Diese Bilder findet auch der Crawler und möchte in der Vorschau eine Galerie anzeigen. Das ist aber nicht immer gewünscht. Nun kann man mit Meta-Informationen ein bestimmtes Bild erzwingen. Der entsprechende Metatag ist:

<meta property="og:image" content="http://komplette.url/zum/bild.jpg" />

Wichtig zu beachten: Das Bild muss mindestens 200 x 200 Pixel groß sein. Und: Größe und Seitenverhältnis definieren, ob das Vorschaubild links neben den anderen Metainformationen angezeigt wird (quadratisches Bild) oder ob sich das Bild über die komplette Breite des Posts zieht.

Attribute in Metadaten prüfen

Heute wurde mir eine gute Frage zu einem Problem gestellt, das ich bisher als trivial abgetan habe: Wie ermittle ich in ASP.NET für eine Eigenschaft des Models, ob dort ein bestimmtes Attribut gesetzt ist? Solche Attribute (insbesondere die vom Typ ValidationAttribute) werden ja verwendet, um Eingabevalidierung vorzunehmen.

Einstiegspunkt soll eine ExtensionMethod auf HtmlHelper sein, wie man sie in ASP.NET häufig findet und die einfach nur einen Text in der Html-Seite ausgeben soll.
public static MvcHtmlString RequiredMark<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
    var required = expression.IsRequired();
    return new MvcHtmlString((!required ? "kein " : string.Empty) + "Pflichtfeld");
}
Spannender Teil daran ist jetzt die schon verwendete ExtensionMethod IsRequired, die für die eigentliche Magie zuständig ist. Diese ist auch noch recht trivial: Sie prüft die Eingabe und ruft die noch zu erstellende Methode HasAttribute auf, die dann die eigentliche Prüfung auf das gesuchte Attribute durchführt:
private static bool IsRequired<T, V>(this Expression<Func<T, V>> expression)
{
    var memberExpression = expression.Body as MemberExpression;
    if (memberExpression == null)
        throw new InvalidOperationException("Expression must be a member expression");

    return HasAttribute(memberExpression.Member, typeof(RequiredAttribute));
}
Auch diese Methode ist recht einfach implementiert - schließlich gibt es in der Klasse Attribute eine statische Methode IsDefined, die dafür verwendet werden kann:
private static bool HasAttribute([NotNull] MemberInfo memberInfo, [NotNull] Type attributeType)
{
    if (memberInfo == null)
    {
        throw new ArgumentNullException();
    }

    var isDefinedOnMember = Attribute.IsDefined(memberInfo, attributeType);

    return isDefinedOnMember;
}
Funktioniert super ..... Funktioniert super und deswegen hören hier die meisten Lösungsvorschläge in einschlägigen Foren auch schon auf.... Funktioniert super, so lange man das Attribut direkt auf dem Model definiert. Allerdings gibt es auch die Möglichkeit, Metadatentypen zu definieren, die dann die Attribute beinhalten. Das ist immer dann sinnvoll, wenn das eigentliche Modell automatisch generiert wird - aus einem Designer oder aus einem T4-Template.

Schauen wir uns ein Beispiel an:
public class MyViewModel
{
    [Required]
    public string Name { get; set; }
}
Dafür funktioniert die bisher erstellte Lösung. Aber bei dem folgenden Beispiel schlägt unsere Prüfung fehl:
[MetadataType(typeof(MyViewModelMetaData))]
public class MyViewModel
{
    public string Name { get; set; }
}

public class MyViewModelMetaData
{
    [Required]
    public string Name { get; set; }
}

Wie kann das Problem gelöst werden? Ganz einfach - zusätzlich zu der schon erstellten Prüfung auf direkt gesetzte Attribute muss nun noch dem Metadatenattribut auf dem Typ gefolgt werden und in diesem Typ geprüft werden welche Attribute auf dem gleichnamigen Member gesetzt sind:
private static bool HasAttribute([NotNull] MemberInfo memberInfo, [NotNull] Type attributeType)
{
    if (memberInfo == null)
    {
        throw new ArgumentNullException();
    }

    // hier prüfe ich direkt gesetzte Attribute
    var isDefinedOnMember = Attribute.IsDefined(memberInfo, attributeType);
    if (isDefinedOnMember)
    {
        return true;
    }

    // jetzt wird noch der Metadatentyp geprüft
    var type = GetMetadataType(memberInfo);
    if (type == null)
    {
        return false;
    }

           
    return type.GetProperties().Any(prop => prop.Name.Equals(memberInfo.Name, StringComparison.OrdinalIgnoreCase) && Attribute.IsDefined(prop, attributeType));
}
Nun liefert die erstellte Methode auch in diesem Fall korrekte Ergebnisse.

Happy coding.

Constraints für WebAPI-Parameter

Seit die WebAPI in der Version 2 vorliegt, kann man dort nicht nur basierend auf Konventionen agieren, sondern die Routen auch mit Hilfe der Attribute RoutePrefix und Route definieren. Schauen wir uns ein Beispiel an:

[RoutePrefix("api/v1/books")]
public class BooksController : ApiController {

    [HttpGet]
    [Route("{id}")]
    public IHttpActionResult GetItem(int id)
    {
        // in production mode we would search for the book in database
        if (id == 87653)
        {
            var book = new Book
            {
                Author = "John Grisham",
                Title = "Gray Mountain",
                Id = 87653,
                Isbn = "978-0385537148"
            };
            return Ok(book);
        }

        return NotFound();
    }
}

Aufrufbar ist das Ganze nun über die Route /api/books/87653.


In der Action habe ich definiert, dass die Id vom Datentyp int sein soll. Was passiert, wenn hier kein int übergeben wird, also wenn hier z.B. die ISBN übergeben wird?


ASP.NET kann den übergebenen Wert nicht mappen und unser Parameter ist nicht Nullable. Deswegen wird der Request mit dem Statuscode 400 abgewiesen.

WebAPI unterstützt auch sogenannte Constraints. Damit kann näher spezifiziert werden, welche Bedingungen die Parameter erfüllen müssen. In meinem Fall ist der Parameter id ein int. Das definiere ich nun zusätzlich in der Route.
[HttpGet]
[Route("{id:int}")]
public IHttpActionResult GetItem(int id)
{
    // do all the fancy stuff
}
Was passiert nun, wenn ein Request mit der ISBN ankommt?


Der Request wird noch immer abgewiesen, aber dieses mal nicht mit Statuscode 400 sondern mit dem Statuscode 404, denn nun wird bereits bei der Auswertung der Routentabelle erkannt, dass keine Route vorliegt, die für diesen Request passt.

Solche Constraints können auch kombiniert werden. Dazu verbindet man verschiedene Constraints einfach mit einem Doppelpunkt. 

[HttpGet]
[Route("{id:int:min(1):max(100000)}")]
public IHttpActionResult GetItem(int id)
{
    // do all the fancy stuff
}

Eine detaillierte Liste der möglichen Constraints können in der MSDN nachgelesen werden.

Wem die möglichen Constraints nicht ausreichen, dem steht es frei, eigene Constraints zu schreiben. Ich möchte das am Beispiel eines ISBN13-Constraints machen. Das Vorgehen ist ganz einfach. 

Zunächst erstellt man eine neue Klasse die das Interface IHttpRouteConstraint implementiert:
public class Isbn13Constraint : IHttpRouteConstraint
{
    private static bool IsIsbn13Valid(string inputValue) { 
        // remove delimiters 
        var isbn = Regex.Replace(inputValue, @"-|\.| ", ""); 

        // validate number of digits 
        if (isbn.Length != 13)
        {
            return false;
        }
        // calculate the product of the digit multiplication operations 
        var product = 0;
        for (var i = 0; i < (isbn.Length - 1); i++)
        {
            product += Convert.ToInt16(isbn.Substring(i, 1)) * (1 + (2 * (i % 2)));
        } 

        // calculate the check digit 
        var  checkdigit = Convert.ToString(10 - (product % 10)); 

        // validate check digit 
        return (checkdigit == isbn.Substring(isbn.Length - 1, 1)); 
    } 

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
        IDictionary values, HttpRouteDirection routeDirection)
    {
        object value;
        if (!values.TryGetValue(parameterName, out value) || value == null)
        {
            return false;
        }

        var stringValue = value as String;
        return stringValue != null && IsIsbn13Valid(stringValue);
    }
}

Der Rückgabewert der einzige Methode Match ist selbsterklärend - true wenn alles passt, false wenn nicht. Der hier eben erzeigte Constraint übernimmt gleich noch die Validierung der Gültigkeit der ISBN. Zugegeben - man kann sich darüber streiten, ob das in das Aufgabengebiet eines Constraints fällt. Schon allein wegen des Rückgabecodes "Not Found" statt "Bad Request" würde ich diese Prüfung eher woanders ansiedeln.

Dieser neue Constraint muss nun in der WebApiConfig noch registriert werden. Statt MapHttpAttributeRoutes ohne Parameter aufzurufen, übergeben wir hier einen neuen ConstraintResolver, der den eben erstellten Constraint enthält.

var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("isbn13", typeof(Isbn13Constraint));

config.MapHttpAttributeRoutes(constraintResolver);
Das war's auch schon fast. Der Constraint kann nun ebenso wie auch die anderen Constraints verwendet werden.

[HttpGet]
[Route("{isbn:isbn13}")]
public IHttpActionResult GetItem(string isbn)
{
    // do all the fancy stuff
}
Happy coding.

Mit dem Service Broker auf Datenänderungen reagieren

Im SQL Server gibt es seit längerem den SQL Server Service Broker. Mit dessen Hilfe kann man live auf Datenänderungen reagieren. Beispiel gefällig?

Ich habe eine sehr einfache Bibliotheksdatenbank. Enthalten ist nur eine Tabelle (dbo.Books) mit den Spalten ID, Author und Title. Und ich habe eine WPF-Anwendung in der in der MainView per Entitiy Framework die Bücher geladen und an die Liste gebunden werden.
<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListBox ItemsSource="{Binding BookList}" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Title}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

public class MainViewModel
{
    public MainViewModel()
    {
        ReloadData();
    }

    private void ReloadData()
    {
        using (var entitites = new LibraryEntities())
        {
            bookList.Clear();
            foreach (var book in entitites.Books)
            {
                bookList.Add(book);   
            }
        }
    }

    private readonly ObservableCollection bookList = new ObservableCollection();

    public ObservableCollection BookList
    {
        get
        {
            return bookList;
        }
    }
}

So weit erst einmal kein Hexenwerk. Der spannende Teil kommt jetzt.

Seit ADO.NET 2.0 gibt es die Klasse SqlDependency, mit der Änderungen an den Daten überwacht werden können. Diese kann für diesen Zweck auch hier verwendet werden.

SqlDependency verfügt über 2 statische Methoden Start und Stop und wie der Name schon vermuten lässt, kann man damit das Tracking starten und auch wieder beenden. In meinem Beispiel sind diese im Konstrukor und im Dispose des ViewModels. Der restliche Code wird in der Reload-Methode angefügt...
public class MainViewModel : IDisposable
{
    private string connectionString;

    public MainViewModel()
    {
        connectionString = new LibraryEntities().Database.Connection.ConnectionString;
        dispatcher = Dispatcher.CurrentDispatcher;
        SqlDependency.Start(connectionString);
        ReloadData();
    }

    private void ReloadData()
    {
        using (var entitites = new LibraryEntities())
        {
            bookList.Clear();
            foreach (var book in entitites.Books)
            {
                bookList.Add(book);   
            }
        }

        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();
            using (var command = new SqlCommand("SELECT Title, Author FROM dbo.Books", connection))
            {
                command.Notification = null;
                var dependency = new SqlDependency(command);
                dependency.OnChange += OnDependencyChange;
                command.ExecuteReader(CommandBehavior.CloseConnection);
            }
        }
    }

    private void OnDependencyChange(object s, SqlNotificationEventArgs e)
    {
        ((SqlDependency)s).OnChange -= OnDependencyChange;

        if (e.Type == SqlNotificationType.Change)
        {
            dispatcher.Invoke(this.ReloadData);
                
        }
    }

    private readonly ObservableCollection<Books> bookList = new ObservableCollection<Books>();

    private Dispatcher dispatcher;

    public ObservableCollection<Books> BookList
    {
        get
        {
            return bookList;
        }
    }

    public void Dispose()
    {
        SqlDependency.Stop(connectionString);
    }
}

... und hier beginnt es etwas schmutzig zu werden.

SqlDependency basiert auf den Möglichkeiten von ADO.NET 2.0. Damit wird hier eine SqlConnection benötigt und ein SqlCommand, das mit einem DataReader auch ausgeführt werden muss. Und es gibt noch ein paar weitere Punkte zu beachten:
  1. Auf der Datenbank muss der Broker aktiviert sein.
  2. Der Benutzer benötigt ausreichende Berechtigungen (SUBSCRIBE QUERY NOTIFICATIONS)
  3. Der Command bei der Initialisierung der SqlDependency muss bestimmten Voraussetzungen entsprechen (also z.B. nicht SELECT * FROM ....)

Alle diese Einschränkungen kann man ausführlich nochmal auf CodeProject nachlesen.

Während der Ausführung ist es dann wichtig zu wissen, dass der Event ein One-Shot ist - heißt er wird nur bei der ersten Änderung ausgelöst. Deshalb muss dann wenn die Daten neu geladen werden auch der Event wieder registriert werden.

Wenn man das alles berücksichtigt, dann kann man damit aber recht coole Sachen machen. Und man ist nicht auf die WPF beschränkt. Mit der Hilfe von OWIN und SignalR auch weitreichender über Datenänderungen informieren.

HTTPS-Redirect - aber konfigurierbar bitte

Manchmal möchte man erzwingen, dass komplette Seiten oder Subdomains - oder um es anders zu formulieren: ganze ASP.NET Webanwendungen - nur per HTTPS erreichbar sind. Real-World-Beispiele gibt es zuhauf: Banking-Seiten, Shops usw.

Eine Möglichkeit ist es, das Ganze fix im Code zu verdrahten. In ASP.NET gibt es ein Filter-Attribut, das genau diesen Redirect vornimmt - das RequireHttpsAttribute. Dieses kann man einfach über die Controller oder Actions schreiben oder eben für den hier betrachteten Fall als globalen Filter definieren.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        // für die ganze Seite HTTPS erzwingen
        filters.Add(new RequireHttpsAttribute());
    }
}

Nun möchte man aber vielleicht nicht immer auf die HTTPS-Seite weiterleiten, sondern nur in bestimmten Fällen. Ich würde beispielsweise gern auf meiner Entwicklungsmaschine auf SSL und den Aufwand den man dafür betreiben muss verzichten. Und ich hätte es gern konfigurierbar. Eine andere Instanz der Anwendung braucht dieses strikte Verhalten vielleicht nicht.

Dafür gibt es auch was - nämlich die web.config. Unter dem Key system.webServer können Redirects definiert werden. Ein entsprechender Eintrag könnte beispielsweise wie folgt aussehen:
<system.webServer>
  <rewrite>
    <rules>
      <rule name="HTTP to HTTPS redirect" stopProcessing="true">
        <match url="^(.*)$"/>
        <conditions>
          <add input="{HTTPS}" pattern="^OFF$" ignoreCase="true" />
          <add input="{HTTP_HOST}" matchType="Pattern" pattern="^localhost(:\d+)?$" negate="true" />
          <add input="{HTTP_HOST}" matchType="Pattern" pattern="^127\.0\.0\.1(:\d+)?$" negate="true" />
        </conditions>
        <action type="Redirect" redirectType="Permanent" url="https://{HTTP_HOST}/{R:1}" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>

Was wird hier gemacht? Wie der Name schon andeutet, wird ein permantener (also Statuscode 301) Redirect vorgenommen - allerdings mit ein paar Ausnahmen: Wenn der Host localhost oder 127.0.0.1 ggf. noch gefolgt von einem Port ist, wird auf den Redirect verzichtet, denn dann befinde ich mich entweder auf dem Webserver oder auf der Entwicklungsmaschine und greife darüber zu. Eigentlich ganz einfach - oder?

Clickjacking und was man dagegen tun kann

"Unsere neue Webanwendung ist anfällig für clickjacking" - nachdem ich diese Information bekam, musste ich zugegebenermaßen erstmal ganz scharf überlegen, was man mir damit sagen wollte.

Was steckt dahinter?
Lädt man eine fremde Seite in einem iframe, so kann man darüber transparente Steuerelemente legen. Der Benutzer denkt nun, er interagiert mit der eigentlichen Anwendung, klickt aber in Wahrheit auf die transparenten Elemente und löst so Aktionen aus, die er nicht auslösen möchte. Ein eher harmloses Beispiel dafür wäre ein transparenter Like-Button für eine fremde Facebookseite der z.B. über einem Google-Suchformular liegt und so statt der Suche erst einmal den Like auslöst.

Wie begegnet man nun diesem Problem?
Es gibt mehrere Ansätze, die man verfolgen kann. Eine in meinen Augen sehr gute Zusammenfassung dafür bietet dieser Blogpost. Erste Möglichkeit ist es, JavaScript zu verwenden und so dem Browser das Laden der Seite im iframe zu verbieten. Neuere Browser (lt. Wikipedia Internet Explorer ab 8.0, Firefox ab 3.6.9, Opera ab 10.50, Safari ab 4.0 und Chrome ab 4.1.249.1042) unterstützen zudem den X-Frame-Options-Header, den man in der Antwort mitsenden kann, um so die Darstellung im iframe zu unterbinden.

Eine Möglichkeit, dies in ASP.NET-Anwendungen zu verwirklichen findet man in diesem Artikel zum Thema Security. Und wenn man einmal dabei ist, an den Headern herumzuschrauben, kann man auch gleich noch einen Blick auf die anderen Header werfen, die automatisch hinzugefügt werden.

HTTP-Status 400 trotz Inhalt

Der Anwendungsfall: Eine ajax-basierte Anwendung soll im Fehlerfall in den Ajax-Requests zwar einen Inhalt zurückgeben, der zu rendern ist, gleichzeitig aber auch einen von 200 abweichenden Statuscode, damit im Browser z.B. im JavaScript eine Unterscheidung stattfinden kann. Verkürzt und stark vereinfacht sieht das dann aus wie im folgenden Beispiel:

[HttpPost]
public ActionResult BuySomething(BuyProduct model)
{
    if (!ModelState.IsValid)
    {
        Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return this.PartialView(model);
    }

    return this.PartialView("SuccessView", model);
}

Damit kann der Client nun im Fehlerfall zusätzliche Aktionen durchführen.

Im IIS7 funktioniert das out-of-the-box leider nicht, da hier mit Standardeinstellungen der Body-Content in der Antwort ersetzt wird. Um dieses Verhalten anzupassen, gibt es das Attribut existingResponse im Element system.webServer / httpErrors.

<system.webServer>
  <httpErrors existingResponse="PassThrough"></httpErrors>
</system.webServer>

Setzt man den Wert auf PassThrough, ersetzt der IIS den Content nicht, wenn gleichzeitig ein Statuscode >= 400 gesetzt wird.

maxRequestLength vs. maxAllowedContentLength

Wer sich mit Dateiuploads beschäftigt wird sich früher oder später Gedanken über Dateigrößenbeschränkungen machen wollen oder müssen.

Die Einstellung dafür findet sich in der web.config. Jedoch gibt es dabei gewisse Dinge zu beachten:

  1. Die MSDN ist sehr spärlich mit Informationen zu den Stellen an denen Änderungen notwendig sind und den vorhandenen Abhängigkeiten.
  2. Je nach verwendeter IIS-Version sind unterschiedliche Werte anzupassen.

Konfiguration des IIS

Relevant ist das Attribut maxAllowedContentLength im Element system.webServer / security / requestFiltering / requestLimits. Zu beachten ist hier, dass dieser Wert in Byte ist. Für Dateigrößen von 20 MB ist hier daher 20971520 einzutragen.

Konfiguration der ASP.NET Runtime

Relevant ist das Attribut maxRequestLength im Element system.web / httpRuntime. Zu beachten ist, dass dieser Wert in Kilobyte ist. Für Dateigrößen von 20 MB ist hier daher 20480 einzutragen. Wird der IIS 6 verwendet, spielt maxAllowedContentLength keine Rolle und es muss nur maxRequestLength angepasst werden.