ASP.NET Core Attribute to Rate limite an Endpoint0 Reacties

Untitled

An easy codesnippet which might help you to avoid an endpoint being DDOS-ed. It uses MemoryCache, which of course also could be configured to use e.g. a Redis backed, but now it uses a hash table with an int key and a boolean, so it is very lightweight and it works in a fast-fail principle (instead of consulting backend storage first). Also keep in mind, that it works –in memory- so it does not protect an array of endpoints.


Usage:


      [HttpGet("myurl")]
        [ThrottleFilter(Name= "myurl", Milliseconds = 5*1000 )]
        public async Task<ActionResult> MyUrl()





using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Net;

namespace AdcCure.ActionFilters
{
     /// <summary>
     /// Decorates any MVC route that needs to have client requests limited by time.
     /// </summary>
     /// <remarks>
     /// Uses the current Microsoft.Extensions.Caching.Memory extensions
     /// to store each client request to the decorated route, using the least amount of memory requirements possible
     /// </remarks>
     [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
     public class ThrottleFilterAttribute : ActionFilterAttribute
     {

        /// <summary>
         /// A unique name for this Throttle.
         /// </summary>
         /// <remarks>
         /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
         /// Cache is 'hashed' so the memory table is int (key), bool (disallow)
         /// The entry only exists for [Milliseconds] so is a minimal resource requirement
         /// </remarks>
         public string Name { get; set; }

        /// <summary>
         /// The number of Milliseconds clients must wait before executing this decorated route again.
         /// </summary>
         public int Milliseconds { get; set; }

        /// <summary>
         /// A text message that will be sent to the client upon throttling.  You can include the token {n} to
         /// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
         /// </summary>
         public string Message { get; set; }

        public override void OnActionExecuting(ActionExecutingContext c)
         {
             var services = c.HttpContext.RequestServices;
             if (!(services.GetService(typeof(IMemoryCache)) is IMemoryCache memCache))
             {
                 throw new InvalidOperationException($"{this.GetType()} configure a memory cache in startup");
             }
             var logger = default(ILogger);
             var loggerInst = services.GetService(typeof(ILoggerFactory));
             if (loggerInst is ILoggerFactory factory)
             {
                 logger = factory.CreateLogger<ThrottleFilterAttribute>();
             }

            var headers = c.HttpContext.Request.Headers;
             var testProxy = headers.ContainsKey("X-Forwarded-For");
             var ipStream = new MemoryStream(512);
             var ip = default(IPAddress);
             if (testProxy)
             {
                 IPAddress.TryParse(headers["X-Forwarded-For"], out ip);
             }
             if (ip == null)
             {
                 ip = c.HttpContext.Connection.RemoteIpAddress;
             }
             if (!string.IsNullOrEmpty(Name))
             {
                 var stringBf = System.Text.Encoding.UTF8.GetBytes(Name);
                 ipStream.Write(stringBf, 0, stringBf.Length);
             }
             var bytes = ip.GetAddressBytes();
             ipStream.Write(bytes, 0, bytes.Length);
            
             var key = ComputeHash(ipStream.ToArray());

            memCache.TryGetValue(key, out bool forbidExecute);

            memCache.Set(key, true, new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(Milliseconds) });
             if (logger != null)
             {
                 logger.LogDebug("key for Throttle {0}", key);
             }
             if (forbidExecute)
             {
                 if (string.IsNullOrEmpty(Message))
                 {
                     Message = $"You may only perform this action every {Milliseconds}ms.";
                 }
                 if (logger != null)
                 {
                     logger.LogWarning("ip {0}, route {1} was denied execute at {2}", ip, Name ?? "unknown", DateTimeOffset.Now);
                 }
                 c.Result = new ContentResult { Content = Message, ContentType = "text/plain" };
                 // see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
                 c.HttpContext.Response.StatusCode = StatusCodes.Status409Conflict;
             }
         }
         /// <summary>
         /// fast method to calc a byte array to the same hash no matter on which process it is performed
         /// </summary>
         /// <param name="data"></param>
         private static int ComputeHash(byte[] data)
         {
             unchecked
             {
                 const int p = 16777619;
                 int hash = (int)2166136261;
                 var len = data.Length;
                 for (int i = 0; i < len; i++)
                     hash = (hash ^ data[i]) * p;

                hash += hash << 13;
                 hash ^= hash >> 7;
                 hash += hash << 3;
                 hash ^= hash >> 17;
                 hash += hash << 5;
                 return hash;
             }
         }
     }
}

Upgrade to ASP.NET Core 3.0 considerations0 Reacties

Before you change .NET Core framework from 2.x to 3.0/3.1, expect for those solutions, issues. More than 1.x to 2.x.

I’ll mention some things to consider.


JSON Serialisation

JSON serialisation is up to 4 times faster due to Microsoft implementing utf8 streams, which is more efficient. Newtonsoft now is an ‘option’.

However, if you have REST Endpoints that work with JObject as argument will not simply work. The suggested JsonElement alternative by MS will require you to do inefficient things. If this is the case, set Newton as default, until .NET supports JsonDocument instead of JObject.

If your POCO objects, used by your REST Api, have self-refering references, System.Text.Json.JsonSerializer cannot handle this.

You need this in startup:

      services.AddNewtonsoftJson(options =>
                    {
                        var opt = options.SerializerSettings;                
                        opt.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
                    })


Synchronous operations are disallowed

If any of your code is still using Xml serialisation, like using XDocument, this will fail if it uses the synchoneous version. Like ‘XDocument.Save’  or even ‘ XDocument.SaveAsync’ That fails with “Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead”


Fix that with:

var xmlWriter = XmlWriter.Create(context.HttpContext.Response.Body, settings: new XmlWriterSettings { Async = true });

await Xml.WriteToAsync(xmlWriter, default);
  await xmlWriter.FlushAsync();


Dont forget to flush. It took me 1 hour to find out. (Yep, no unit tests, extern solution).

However, some libraries are out of your control, so you might need these too.


services.Configure(options =>
             {
                 options.AllowSynchronousIO = true; //to get metaweblogcontroller working
             });
             services.Configure(options =>
             {
                 options.AllowSynchronousIO = true;
             });

UrlHelper

If you work with Razor, you might be using UrlHelper. In previous .NET core versions you could inject IUrlHelper using IActionContextAccessor. You need an extra line using IUrlHelperFactory.

services.AddScoped(x => {
                var actionContext = x.GetService().ActionContext;
                var factory = x.GetRequiredService();
                return factory.GetUrlHelper(actionContext);
            });


    An easy Taghelper to make an element ‘active’ when clicked using Razor.0 Reacties

    Still using backend HTML logic.

    ASP.NET Core makes things a lot more easier for us, backend developers. Though, accent today, is more on frontend development, still, for reasons of security for instance, backend state management is still used.

    Now take a look at this tiny gem, a TagHelper.

    A ‘which menu has been clicked-set-active-helper’

    What it does, basically, is checking if a

  • element has an attribute activecheck. If so, it detects which ‘action-route’ was executed/is executing and if the name matches, it adds class=”active”. So the output will be  e.g.
  • active”>
  • e.g. in the Razor View I have:


                                           <li activecheck="true" data-menu="Index" >
                                                <a asp-controller="Home" asp-action="Index">Homea>
                                           

      I had to add a ‘data-menu’ attribute, because Taghelpers, can not, as far as I found, parse the raw Razor Markup, when using ‘GetChildContentAsync()’  for instance, it only gets the HTML output that’s already there, without Razor markup. So, I cannot parse the child anchor and retrieve asp-acction=”Index”.
      Therefore, I specify it again using data-menu=”Index”. Note, this is removed later by the taghelper.

      using Microsoft.AspNetCore.Html;
      using Microsoft.AspNetCore.Mvc.Controllers;
      using Microsoft.AspNetCore.Mvc.Infrastructure;
      using Microsoft.AspNetCore.Razor.TagHelpers;

      namespace ladidah.TagHelpers
      {
          [HtmlTargetElement("li", Attributes = "activecheck")]
          public class LiActiveHelper: TagHelper
          {
              public bool activecheck { get; set; }
              private readonly string menu;
              public LiActiveHelper(IActionContextAccessor httpContext)
              {
                  var currentRoute = (ControllerActionDescriptor)httpContext.ActionContext.ActionDescriptor;
                  menu = currentRoute.ActionName;
              }
              public override void Process(TagHelperContext context, TagHelperOutput output)
              {
                  if (activecheck)
                  {
                      var dataMenu = output.Attributes["data-menu"];
                   
                       if (dataMenu != null && dataMenu.Value is HtmlString str && str.Value == menu)
                      {
                          output.Attributes.RemoveAll("activecheck");
                          output.Attributes.RemoveAll("data-menu");
                          if (!output.Attributes.ContainsName("class"))
                          {
                              output.Attributes.SetAttribute("class", "active");
                          }
                      }               
                  }          
              }
          }
      }

      To activate this taghelper, I had to add the following line in my _ViewImports.cshtml file

      @addTagHelper ladidah.TagHelpers.LiActiveHelper, ladidah

      Benefit of IoC in ASP.NET Core.

      In my ConfigureServices method, in startup.cs I had to add the following line, in order to be able to get the action context of the current MVC Controller.

      public void ConfigureServices(IServiceCollection services)
             {

      // code removed for readability

        services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

      }

      I hope you find this snippet useful.

    Finally OData 7.x supports and ASP.NET Core 2.x0 Reacties

    Finally we can create OData (v4) services using ASP.NET Core.

    My first impression, it’s ok, plus it has extra features.

    Use my startup project, on Github as a sample. (It’s tricky to forget to enable features like Filter, otherwise you’ll and up with :

    value":"The query specified in the URI is not valid. The property 'Id' cannot be used in the $filter query option."}

    So, this is the Configure section

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
           {
               if (env.IsDevelopment())
               {
                   app.UseDeveloperExceptionPage();
               }
               var builder = new ODataConventionModelBuilder(app.ApplicationServices);
               builder.EntitySet("Cars");
          
               app.UseMvc(routeBuilder =>
               {
                   routeBuilder.Count().Filter().OrderBy().Select().MaxTop(null).Expand();
                   routeBuilder.MapODataServiceRoute("ODataRoute", "odata", builder.GetEdmModel());
                   // Work-around for #1175

                   routeBuilder.EnableDependencyInjection();
               } ) ;
           }

    To get PATCH and POST working, we need the [FromBody] Attribute like here

    public async Task Post([FromBody] Car newCar)

     

    The sample, is downloadable here (ASP.NET-Core-With-Odata-7)

    Howto get the ASP.NET Core ContentRoot path?0 Reacties

     

    There are 3 ways that I have found to be working within Startup.cs

    1. use Environment variables.  This however, only works if you also have added the environmentvariables. (builder.AddEnvironmentVariables(); )
    2. var env = Configuration.GetSection("ASPNETCORE_CONTENTROOT");
      var path =     env.Value

    3. Resolve IHostingEnvironment from Services.
        var sv = (IHostingEnvironment)services.FirstOrDefault(f => f.ServiceType == typeof(IHostingEnvironment)).ImplementationInstance;
      var path = sv.ContentRootPath;
      Though, I can imagine you would rather resolve HostingEnvironment but this does not work during ConfigureServices scope..
    4. Use Directory
      eg:  var path = Directory.GetCurrentDirectory();

    ASP.NET Core MVC. Howto set Casing strategy for JSON0 Reacties

    While converting a previous piece of code, written using Mvc 4.x I found that the output of the Json previously, when set no option, was unchanged, so a class with a property say, Name would output in JSON as ‘Name’.

    However, ASP.NET Core Mvc
    when returning JSon results, it now defaults to camelCasing.

     

    My output had to be:

    {
        "page": 1,
        "records": 2,
        "rows": [
            {
                "Id": 2,
                "Name": "asdfasdf",
                "UrlSlug": "asdfasdf",
                "Description": "asdf"
            },
            {
                "Id": 1,
                "Name": "Bkaat",
                "UrlSlug": "Blaat",
                "Description": "zdf
    asdfasdf asdf a"
            }
        ],
        "total": 1
    }

    but it was camelCased.

     

    So, HOW to set this to the previous behavior (its old software, so I won’t change everything)

    Simple like this:

    public void ConfigureServices(IServiceCollection services)

    {

        services.AddMvc().AddJsonOptions(options => options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver() { NamingStrategy = new DefaultNamingStrategy()});

     

    And finding further options, you also can set it to SnakeCaseNamingStrategy (this is like ‘url_slug’ etc)

    And of course, CamelCaseNamingStrategy, which is the default.

    I hope this saves you some time, and you don’t need to implement a custom contract resolver.