Dies und Das in ASP.NET Core

Immutable Objects aus dem Path

Für das nachvollziehen von Fehlern in einem Code gibt es nichts angenehmeres als Immutable Objects, da sie den State nach dem Erzeugen nicht mehr ändern können. Immutable Objects werden per se nicht von .NET unterstützt (durch Reflection kann man sie immer ändern) – aber man kann durch e.g. Private-Setter einen ähnlichen Effekt erreichen. Auch AOP Postsharp bietet ein Immutable Threading Model. In ASP.NET Core musste ich ein wenig suchen – daher hier ein Beispiel:

namespace WebApplication1.Controllers
{
    [Route("api")]
    public class ValuesController : Controller
    {
        [HttpPut("projects/{projectId}")]
        public void Put(ProjectId projectId)
        {
        }
    }
}

namespace WebApplication1.Models
{
    [ModelBinder(BinderType = typeof(MyValueBinder))]
    public class ProjectId
    {
        public ProjectId(string value)
        {
            var match = Regex.Match(value, @"^PROID-\d{3}$");

            if (match.Success)
            {
                Value = value;
            }
            else
            {
                throw new ArgumentException(nameof(value));
            }
        }

        public string Value { get; }
    }
}

namespace WebApplication1.Models
{
    public class MyValueBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;

            bindingContext.Result = ModelBindingResult.Success(new ProjectId(valueProviderResult));

            return Task.CompletedTask;
        }
    }
}

Das einzig unschöne ist, dass „Swashbuckle.AspNetCore“ durch den ApiExplorer die Klasse und den Path-Param separat anzeigt. Das hab ich durch einen IOperationFilter entfernen bekommen.

HTTP Status Codes 400, 422, 409

Eine Sehr interessante Diskussion hatte ich auch Rund um HTTP Status Codes: Welchen Status-Code schickt man, wenn die Synatax der Nachricht passt, aber der Inhalt nicht?

  • 400: The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
  • 422: The 422 (Unprocessable Entity) status code means the server understands the content type of the request entity (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was unable to process the contained instructions. For example, this error condition may occur if an XML request body contains well-formed (i.e., syntactically correct), but semantically erroneous, XML instructions.
  • 409: The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource. This code is used in situations where the user might be able to resolve the conflict and resubmit the request.

Wir haben uns daher für folgendes entschieden:

  • 422 für die diskutierte Situation (Syntax ok – aber Inhalt falsch)
  • 409 für Optimistic-Concurrency Fehler
  • 400 für andere Probleme Rund um die Nachricht (Client-Fehler)

JSON.NET Tolerant Enum Parser

In einigen Fällen stimmt die Regel:

Be conservative in what you send, be liberal in what you accept

Ist man von einem Enum nur an 3 Werten interessiert (und es gibt viel mehr bzw. niemand kann sagen, welche States es überhaupt gibt – was auch schon ein Smell ist), dann kann folgendes Snippet helfen:

[JsonConverter(typeof(TolerantStringEnumConverter))]
[DefaultValue(Unknown)]
public enum ZonenArt
{
  [EnumMember(Value = "VAL1")]
  Value1,

  [EnumMember(Value = "VAL2")]
  Value2,

  [EnumMember(Value = "VAL3")]
  Value3,

  // There are many others
  Unknown
  }


public sealed class TolerantStringEnumConverter : StringEnumConverter
{
  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    try
    {
      return base.ReadJson(reader, objectType, existingValue, serializer);
    }
    catch (JsonSerializationException)
    {
      var defaultValueAttribute = objectType.GetCustomAttribute(typeof(DefaultValueAttribute)) as DefaultValueAttribute;

      if (defaultValueAttribute != null)
      {
        return defaultValueAttribute.Value;
      }

      throw;
    }
  }
}

Rekursives Abfragen von Objekten

Mit SelectMany kann man die unmittelbaren Kinder eines Objects abfragen. Aber was ist, wenn man einen ganzen Baum hat? Man findet viele Lösungen im Internet – eine davon ist von Eric Lippert: https://stackoverflow.com/questions/11830174/how-to-flatten-tree-via-linq/20335369#20335369

using System;
using System.Collections.Generic;
using System.Linq;
using MoreLinq;

namespace ConsoleApp1
{
    public static class EnumerableExtensions
    {
        public static IEnumerable<T> Traverse<T>(this T root, Func<T, IEnumerable<T>> selector)
        {
            var stack = new Stack<T>();

            stack.Push(root);

            while (stack.Count > 0)
            {
                var current = stack.Pop();

                yield return current;

                foreach (var child in selector(current))
                {
                    stack.Push(child);
                }
            }
        }
    }

    class Node
    {
        public Node(string nodeName)
        {
            NodeName = nodeName;
        }

        public string NodeName { get; }

        public IList<Node> Childs { get; } = new List<Node>();
    }

    class Program
    {
        static void Main(string[] args)
        {
            var root = new Node("A")
            {
                Childs =
                {
                    new Node("A.A")
                    {
                        Childs =
                        {
                            new Node("A.A.A")
                        }
                    },
                    new Node("A.B"),
                    new Node("A.C")
                    {
                        Childs =
                        {
                            new Node("A.C.A"),
                            new Node("A.C.B")
                        }
                    },
                }
            };

            root.Traverse(n => n.Childs).ToArray().ForEach(x => Console.WriteLine(x.NodeName));

            Console.ReadKey();
        }
    }
}

In MoreLINQ wurde es schon vorgeschlagen aber bis dato noch nicht umgesetzt.

Structred Logging

Kibana und Serilog funktionieren super, da man sehr elegant nach gewissen Properties suchen kann. Diese können in den Log-Context gepushed („Enrichers“) werden und werden dann in jeder Log-Zeile angezeigt (es gibt auch ein paar fertige Plugins). Ein Beispiel zeigt die Verwendung eines Enrichers.

File-Sink mit Rolling Policy gibt es auch.