diff --git a/src/Entity/HttpHeaderCollection.cs b/src/Entity/HttpHeaderCollection.cs new file mode 100644 index 0000000..9005ebf --- /dev/null +++ b/src/Entity/HttpHeaderCollection.cs @@ -0,0 +1,478 @@ +// The Sisk Framework source code +// Copyright (c) 2023 PROJECT PRINCIPIUM +// +// The code below is licensed under the MIT license as +// of the date of its publication, available at +// +// File name: HttpHeaderCollection.cs +// Repository: https://github.com/sisk-http/core + + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Sisk.Core.Http; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Header = Sisk.Core.Internal.HttpKnownHeaderNames; + +namespace Sisk.Core.Entity; + +/// +/// Represents an collection of HTTP headers with their name and values. +/// +/// +/// public class HttpHeaderCollection : NameValueCollection +/// +/// +/// Class +/// +public class HttpHeaderCollection : NameValueCollection +{ + /// + /// Create an new instance of the class. + /// + public HttpHeaderCollection() + { + } + + /// + /// Creates an new instance of the with the specified + /// headers. + /// + /// The header collection. + public HttpHeaderCollection(NameValueCollection headers) : base(headers) + { + } + + /// + /// Gets or sets the value of the HTTP Accept header. + /// + /// + /// public string? Accept { get; set; } + /// + /// + /// Property + /// + public string? Accept { get => base[Header.Accept]; set => base[Header.Accept] = value; } + + /// + /// Gets or sets the value of the HTTP Accept-Charset header. + /// + /// + /// public string? AcceptCharset { get; set; } + /// + /// + /// Property + /// + public string? AcceptCharset { get => base[Header.AcceptCharset]; set => base[Header.AcceptCharset] = value; } + + /// + /// Gets or sets the value of the HTTP Accept-Encoding header. + /// + /// + /// public string? AcceptEncoding { get; set; } + /// + /// + /// Property + /// + public string? AcceptEncoding { get => base[Header.AcceptEncoding]; set => base[Header.AcceptEncoding] = value; } + + /// + /// Gets or sets the value of the HTTP Accept-Language header. + /// + /// + /// public string? AcceptLanguage { get; set; } + /// + /// + /// Property + /// + public string? AcceptLanguage { get => base[Header.AcceptLanguage]; set => base[Header.AcceptLanguage] = value; } + + /// + /// Gets or sets the value of the HTTP Accept-Patch header. + /// + /// + /// public string? AcceptPatch { get; set; } + /// + /// + /// Property + /// + public string? AcceptPatch { get => base[Header.AcceptPatch]; set => base[Header.AcceptPatch] = value; } + + /// + /// Gets or sets the value of the HTTP Accept-Ranges header. + /// + /// + /// public string? AcceptRanges { get; set; } + /// + /// + /// Property + /// + public string? AcceptRanges { get => base[Header.AcceptRanges]; set => base[Header.AcceptRanges] = value; } + + /// + /// Gets or sets the value of the HTTP Access-Control-Allow-Credentials header. + /// + /// + /// Note: this header can be overwritten by the current configuration. + /// + /// + /// public string? AccessControlAllowCredentials { get; set; } + /// + /// + /// Property + /// + public string? AccessControlAllowCredentials { get => base[Header.AccessControlAllowCredentials]; set => base[Header.AccessControlAllowCredentials] = value; } + + /// + /// Gets or sets the value of the HTTP Access-Control-Allow-Headers header. + /// + /// + /// Note: this header can be overwritten by the current configuration. + /// + /// + /// public string? AccessControlAllowHeaders { get; set; } + /// + /// + /// Property + /// + public string? AccessControlAllowHeaders { get => base[Header.AccessControlAllowHeaders]; set => base[Header.AccessControlAllowHeaders] = value; } + + /// + /// Gets or sets the value of the HTTP Access-Control-Allow-Methods header. + /// + /// + /// public string? AccessControlAllowMethods { get; set; } + /// + /// + /// Property + /// + public string? AccessControlAllowMethods { get => base[Header.AccessControlAllowMethods]; set => base[Header.AccessControlAllowMethods] = value; } + + /// + /// Gets or sets the value of the HTTP Access-Control-Allow-Origin header. + /// + /// + /// Note: this header can be overwritten by the current configuration. + /// + /// + /// public string? AccessControlAllowOrigin { get; set; } + /// + /// + /// Property + /// + public string? AccessControlAllowOrigin { get => base[Header.AccessControlAllowOrigin]; set => base[Header.AccessControlAllowOrigin] = value; } + + /// + /// Gets or sets the value of the HTTP Access-Control-Expose-Headers header. + /// + /// + /// Note: this header can be overwritten by the current configuration. + /// + /// + /// public string? AccessControlExposeHeaders { get; set; } + /// + /// + /// Property + /// + public string? AccessControlExposeHeaders { get => base[Header.AccessControlExposeHeaders]; set => base[Header.AccessControlExposeHeaders] = value; } + + /// + /// Gets or sets the value of the HTTP Access-Control-Max-Age header. + /// + /// + /// Note: this header can be overwritten by the current configuration. + /// + /// + /// public string? AccessControlMaxAge { get; set; } + /// + /// + /// Property + /// + public string? AccessControlMaxAge { get => base[Header.AccessControlMaxAge]; set => base[Header.AccessControlMaxAge] = value; } + + /// + /// Gets or sets the value of the HTTP Age header. + /// + /// + /// public string? Age { get; set; } + /// + /// + /// Property + /// + public string? Age { get => base[Header.Age]; set => base[Header.Age] = value; } + + /// + /// Gets or sets the value of the HTTP Allow header. + /// + /// + /// public string? Allow { get; set; } + /// + /// + /// Property + /// + public string? Allow { get => base[Header.Allow]; set => base[Header.Allow] = value; } + + /// + /// Gets or sets the value of the HTTP Alt-Svc header. + /// + /// + /// public string? AltSvc { get; set; } + /// + /// + /// Property + /// + public string? AltSvc { get => base[Header.AltSvc]; set => base[Header.AltSvc] = value; } + + /// + /// Gets or sets the value of the HTTP Authorization header. + /// + /// + /// public string? Authorization { get; set; } + /// + /// + /// Property + /// + public string? Authorization { get => base[Header.Authorization]; set => base[Header.Authorization] = value; } + + /// + /// Gets or sets the value of the HTTP Cache-Control header. + /// + /// + /// public string? CacheControl { get; set; } + /// + /// + /// Property + /// + public string? CacheControl { get => base[Header.CacheControl]; set => base[Header.CacheControl] = value; } + + /// + /// Gets or sets the value of the HTTP Content-Disposition header. + /// + /// + /// public string? ContentDisposition { get; set; } + /// + /// + /// Property + /// + public string? ContentDisposition { get => base[Header.ContentDisposition]; set => base[Header.ContentDisposition] = value; } + + /// + /// Gets or sets the value of the HTTP Content-Encoding header. + /// + /// + /// public string? ContentEncoding { get; set; } + /// + /// + /// Property + /// + public string? ContentEncoding { get => base[Header.ContentEncoding]; set => base[Header.ContentEncoding] = value; } + + /// + /// Gets or sets the value of the HTTP Content-Language header. + /// + /// + /// public string? ContentLanguage { get; set; } + /// + /// + /// Property + /// + public string? ContentLanguage { get => base[Header.ContentLanguage]; set => base[Header.ContentLanguage] = value; } + + /// + /// Gets or sets the value of the HTTP Content-Range header. + /// + /// + /// public string? ContentRange { get; set; } + /// + /// + /// Property + /// + public string? ContentRange { get => base[Header.ContentRange]; set => base[Header.ContentRange] = value; } + + /// + /// Gets or sets the value of the HTTP Content-Type header. + /// + /// + /// Note: setting the value of this header, the value present in the response's will be overwritten. + /// + /// + /// public string? ContentType { get; set; } + /// + /// + /// Property + /// + public string? ContentType { get => base[Header.ContentType]; set => base[Header.ContentType] = value; } + + /// + /// Gets or sets the value of the HTTP Cookie header. + /// + /// + /// Tip: use property to getting cookies values from requests and + /// on to set cookies. + /// + /// + /// public string? Cookie { get; set; } + /// + /// + /// Property + /// + public string? Cookie { get => base[Header.Cookie]; set => base[Header.Cookie] = value; } + + /// + /// Gets or sets the value of the HTTP Expect header. + /// + /// + /// public string? Expect { get; set; } + /// + /// + /// Property + /// + public string? Expect { get => base[Header.Expect]; set => base[Header.Expect] = value; } + + /// + /// Gets or sets the value of the HTTP Expires header. + /// + /// + /// public string? Expires { get; set; } + /// + /// + /// Property + /// + public string? Expires { get => base[Header.Expires]; set => base[Header.Expires] = value; } + + /// + /// Gets or sets the value of the HTTP Host header. + /// + /// + /// public string? Host { get; set; } + /// + /// + /// Property + /// + public string? Host { get => base[Header.Host]; set => base[Header.Host] = value; } + + /// + /// Gets or sets the value of the HTTP Origin header. + /// + /// + /// public string? Origin { get; set; } + /// + /// + /// Property + /// + public string? Origin { get => base[Header.Origin]; set => base[Header.Origin] = value; } + + /// + /// Gets or sets the value of the HTTP Range header. + /// + /// + /// public string? Range { get; set; } + /// + /// + /// Property + /// + public string? Range { get => base[Header.Range]; set => base[Header.Range] = value; } + + /// + /// Gets or sets the value of the HTTP Referer header. + /// + /// + /// public string? Referer { get; set; } + /// + /// + /// Property + /// + public string? Referer { get => base[Header.Referer]; set => base[Header.Referer] = value; } + + /// + /// Gets or sets the value of the HTTP Retry-After header. + /// + /// + /// public string? RetryAfter { get; set; } + /// + /// + /// Property + /// + public string? RetryAfter { get => base[Header.RetryAfter]; set => base[Header.RetryAfter] = value; } + + /// + /// Gets or sets the value of the HTTP Set-Cookie header. + /// + /// + /// Tip: use property to getting cookies values from requests and + /// on to set cookies. + /// + /// + /// public string? SetCookie { get; set; } + /// + /// + /// Property + /// + public string? SetCookie { get => base[Header.SetCookie]; set => base[Header.SetCookie] = value; } + + /// + /// Gets or sets the value of the HTTP User-Agent header. + /// + /// + /// public string? UserAgent { get; set; } + /// + /// + /// Property + /// + public string? UserAgent { get => base[Header.UserAgent]; set => base[Header.UserAgent] = value; } + + /// + /// Gets or sets the value of the HTTP Vary header. + /// + /// + /// public string? Vary { get; set; } + /// + /// + /// Property + /// + public string? Vary { get => base[Header.Vary]; set => base[Header.Vary] = value; } + + /// + /// Gets or sets the value of the HTTP WWW-Authenticate header. + /// + /// + /// public string? WWWAuthenticate { get; set; } + /// + /// + /// Property + /// + public string? WWWAuthenticate { get => base[Header.WWWAuthenticate]; set => base[Header.WWWAuthenticate] = value; } + + /// + /// Gets or sets the value of the HTTP X-Forwarded-For header. + /// + /// + /// Tip: enable the property to obtain the user client proxied IP throught . + /// + /// + /// public string? XForwardedFor { get; set; } + /// + /// + /// Property + /// + public string? XForwardedFor { get => base[Header.XForwardedFor]; set => base[Header.XForwardedFor] = value; } + + /// + /// Gets or sets the value of the HTTP X-Forwarded-Host header. + /// + /// + /// Tip: enable the property to obtain the client requested host throught . + /// + /// + /// public string? XForwardedHost { get; set; } + /// + /// + /// Property + /// + public string? XForwardedHost { get => base[Header.XForwardedHost]; set => base[Header.XForwardedHost] = value; } + +} diff --git a/src/Ext/CircularBuffer.cs b/src/Ext/CircularBuffer.cs index 126ef38..926c0bb 100644 --- a/src/Ext/CircularBuffer.cs +++ b/src/Ext/CircularBuffer.cs @@ -9,403 +9,70 @@ using System.Collections; - -/// -internal class CircularBuffer : IEnumerable +namespace System; + +/// +/// Represents an thread-safe, fixed-capacity circular buffer that stores elements of . +/// +/// The type of elements stored in the buffer. +/// +public class CircularBuffer : IEnumerable { - private readonly T[] _buffer; - - /// - /// The _start. Index of the first element in buffer. - /// - private int _start; - - /// - /// The _end. Index after the last element in the buffer. - /// - private int _end; + readonly T[] items; - /// - /// The _size. Buffer size. - /// - private int _size; + int capacity = 0; /// - /// Initializes a new instance of the class. - /// + /// Creates an new instance of the with the specified + /// capacity. /// - /// - /// Buffer capacity. Must be positive. - /// + /// The circular buffer capacity. public CircularBuffer(int capacity) - : this(capacity, new T[] { }) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// Buffer capacity. Must be positive. - /// - /// - /// Items to fill buffer with. Items length must be less than capacity. - /// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from - /// any enumerable. - /// - public CircularBuffer(int capacity, T[] items) - { - if (capacity < 1) - { - throw new ArgumentException( - "Circular buffer cannot have negative or zero capacity.", nameof(capacity)); - } - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - if (items.Length > capacity) - { - throw new ArgumentException( - "Too many items to fit circular buffer", nameof(items)); - } - - _buffer = new T[capacity]; - - Array.Copy(items, _buffer, items.Length); - _size = items.Length; - - _start = 0; - _end = _size == capacity ? 0 : _size; - } - - /// - /// Maximum capacity of the buffer. Elements pushed into the buffer after - /// maximum capacity is reached (IsFull = true), will remove an element. - /// - public int Capacity { get { return _buffer.Length; } } - - /// - /// Boolean indicating if Circular is at full capacity. - /// Adding more elements when the buffer is full will - /// cause elements to be removed from the other end - /// of the buffer. - /// - public bool IsFull { - get - { - return Size == Capacity; - } + this.capacity = capacity; + items = new T[capacity]; } /// - /// True if has no elements. + /// Adds an object of into the beginning of the circular buffer and pushes + /// old items to end. /// - public bool IsEmpty + /// The item to add. + public void Add(T item) { - get + lock (items) { - return Size == 0; - } - } - - /// - /// Current buffer size (the number of elements that the buffer has). - /// - public int Size { get { return _size; } } - - /// - /// Element at the front of the buffer - this[0]. - /// - /// The value of the element of type T at the front of the buffer. - public T Front() - { - ThrowIfEmpty(); - return _buffer[_start]; - } - - /// - /// Element at the back of the buffer - this[Size - 1]. - /// - /// The value of the element of type T at the back of the buffer. - public T Back() - { - ThrowIfEmpty(); - return _buffer[(_end != 0 ? _end : Capacity) - 1]; - } - - /// - /// Index access to elements in buffer. - /// Index does not loop around like when adding elements, - /// valid interval is [0;Size[ - /// - /// Index of element to access. - /// Thrown when index is outside of [; Size[ interval. - public T this[int index] - { - get - { - if (IsEmpty) + for (int i = capacity - 1; i > 0; i--) { - throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); + items[i] = items[i - 1]; } - if (index >= _size) - { - throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); - } - int actualIndex = InternalIndex(index); - return _buffer[actualIndex]; - } - set - { - if (IsEmpty) - { - throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); - } - if (index >= _size) - { - throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); - } - int actualIndex = InternalIndex(index); - _buffer[actualIndex] = value; - } - } - - /// - /// Pushes a new element to the back of the buffer. Back()/this[Size-1] - /// will now return this element. - /// - /// When the buffer is full, the element at Front()/this[0] will be - /// popped to allow for this new element to fit. - /// - /// Item to push to the back of the buffer - public void PushBack(T item) - { - if (IsFull) - { - _buffer[_end] = item; - Increment(ref _end); - _start = _end; - } - else - { - _buffer[_end] = item; - Increment(ref _end); - ++_size; - } - } - /// - /// Pushes a new element to the front of the buffer. Front()/this[0] - /// will now return this element. - /// - /// When the buffer is full, the element at Back()/this[Size-1] will be - /// popped to allow for this new element to fit. - /// - /// Item to push to the front of the buffer - public void PushFront(T item) - { - if (IsFull) - { - Decrement(ref _start); - _end = _start; - _buffer[_start] = item; - } - else - { - Decrement(ref _start); - _buffer[_start] = item; - ++_size; + items[0] = item; } } /// - /// Removes the element at the back of the buffer. Decreasing the - /// Buffer size by 1. - /// - public void PopBack() - { - ThrowIfEmpty("Cannot take elements from an empty buffer."); - Decrement(ref _end); - _buffer[_end] = default(T)!; - --_size; - } - - /// - /// Removes the element at the front of the buffer. Decreasing the - /// Buffer size by 1. + /// Returns an array representation of this items in their defined + /// positions within the capacity. /// - public void PopFront() - { - ThrowIfEmpty("Cannot take elements from an empty buffer."); - _buffer[_start] = default(T)!; - Increment(ref _start); - --_size; - } - - /// - /// Clears the contents of the array. Size = 0, Capacity is unchanged. - /// - /// - public void Clear() - { - // to clear we just reset everything. - _start = 0; - _end = 0; - _size = 0; - Array.Clear(_buffer, 0, _buffer.Length); - } - - /// - /// Copies the buffer contents to an array, according to the logical - /// contents of the buffer (i.e. independent of the internal - /// order/contents) - /// - /// A new array with a copy of the buffer contents. - public T[] ToArray() - { - T[] newArray = new T[Size]; - int newArrayOffset = 0; - var segments = ToArraySegments(); - foreach (ArraySegment segment in segments) - { - Array.Copy(segment.Array!, segment.Offset, newArray, newArrayOffset, segment.Count); - newArrayOffset += segment.Count; - } - return newArray; - } + public T[] ToArray() => items; /// - /// Get the contents of the buffer as 2 ArraySegments. - /// Respects the logical contents of the buffer, where - /// each segment and items in each segment are ordered - /// according to insertion. - /// - /// Fast: does not copy the array elements. - /// Useful for methods like Send(IList<ArraySegment<Byte>>). - /// - /// Segments may be empty. + /// Gets the current capacity of this . /// - /// An IList with 2 segments corresponding to the buffer content. - public IList> ToArraySegments() - { - return new[] { ArrayOne(), ArrayTwo() }; - } + public int Capacity { get => capacity; } - #region IEnumerable implementation - /// - /// Returns an enumerator that iterates through this buffer. - /// - /// An enumerator that can be used to iterate this collection. + /// + /// public IEnumerator GetEnumerator() { - var segments = ToArraySegments(); - foreach (ArraySegment segment in segments) - { - for (int i = 0; i < segment.Count; i++) - { - yield return segment.Array![segment.Offset + i]; - } - } - } - #endregion - #region IEnumerable implementation - IEnumerator IEnumerable.GetEnumerator() - { - return (IEnumerator)GetEnumerator(); - } - #endregion - - private void ThrowIfEmpty(string message = "Cannot access an empty buffer.") - { - if (IsEmpty) - { - throw new InvalidOperationException(message); - } + return ((IEnumerable)items).GetEnumerator(); } - /// - /// Increments the provided index variable by one, wrapping - /// around if necessary. - /// - /// - private void Increment(ref int index) - { - if (++index == Capacity) - { - index = 0; - } - } - - /// - /// Decrements the provided index variable by one, wrapping - /// around if necessary. - /// - /// - private void Decrement(ref int index) - { - if (index == 0) - { - index = Capacity; - } - index--; - } - - /// - /// Converts the index in the argument to an index in _buffer - /// - /// - /// The transformed index. - /// - /// - /// External index. - /// - private int InternalIndex(int index) - { - return _start + (index < (Capacity - _start) ? index : index - Capacity); - } - - // doing ArrayOne and ArrayTwo methods returning ArraySegment as seen here: - // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1957cccdcb0c4ef7d80a34a990065818d - // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b - // should help a lot with the code. - - #region Array items easy access. - // The array is composed by at most two non-contiguous segments, - // the next two methods allow easy access to those. - - private ArraySegment ArrayOne() - { - if (IsEmpty) - { - return new ArraySegment(new T[0]); - } - else if (_start < _end) - { - return new ArraySegment(_buffer, _start, _end - _start); - } - else - { - return new ArraySegment(_buffer, _start, _buffer.Length - _start); - } - } - - private ArraySegment ArrayTwo() + /// + /// + IEnumerator IEnumerable.GetEnumerator() { - if (IsEmpty) - { - return new ArraySegment(new T[0]); - } - else if (_start < _end) - { - return new ArraySegment(_buffer, _end, 0); - } - else - { - return new ArraySegment(_buffer, 0, _end); - } + return items.GetEnumerator(); } - #endregion } \ No newline at end of file diff --git a/src/Http/Handlers/HttpServerHandlerRepository.cs b/src/Http/Handlers/HttpServerHandlerRepository.cs index 1ed7d73..4c3f596 100644 --- a/src/Http/Handlers/HttpServerHandlerRepository.cs +++ b/src/Http/Handlers/HttpServerHandlerRepository.cs @@ -13,8 +13,14 @@ namespace Sisk.Core.Http.Handlers; internal class HttpServerHandlerRepository { + private readonly HttpServer parent; private readonly List handlers = new List(); + public HttpServerHandlerRepository(HttpServer parent) + { + this.parent = parent; + } + public void RegisterHandler(HttpServerHandler handler) { handlers.Add(handler); @@ -23,7 +29,18 @@ public void RegisterHandler(HttpServerHandler handler) private void CallEvery(Action action) { foreach (HttpServerHandler handler in handlers) - action(handler); + try + { + action(handler); + } + catch (Exception ex) + { + if (parent.ServerConfiguration.ThrowExceptions) + { + parent.ServerConfiguration.ErrorsLogsStream?.WriteException(ex); + } + else throw; + } } internal void ServerStarting(HttpServer val) => CallEvery(handler => handler.InvokeOnServerStarting(val)); diff --git a/src/Http/HttpRequest.cs b/src/Http/HttpRequest.cs index 7a58abb..caf0c39 100644 --- a/src/Http/HttpRequest.cs +++ b/src/Http/HttpRequest.cs @@ -53,7 +53,7 @@ public sealed class HttpRequest private byte[]? contentBytes; internal bool isStreaming; private HttpRequestEventSource? activeEventSource; - private NameValueCollection? headers = null; + private HttpHeaderCollection? headers = null; private NameValueCollection? cookies = null; private StringValueCollection? query = null; private StringValueCollection? form = null; @@ -84,7 +84,7 @@ void ReadRequestStreamContents() { if (contentBytes == null) { - using (var memoryStream = new MemoryStream()) + using (var memoryStream = new MemoryStream((int)ContentLength)) { listenerRequest.InputStream.CopyTo(memoryStream); contentBytes = memoryStream.ToArray(); @@ -140,12 +140,12 @@ void ReadRequestStreamContents() /// Gets the HTTP request headers. /// /// - /// public NameValueCollection Headers { get; } + /// public HttpHeaderCollection Headers { get; } /// /// /// Property /// - public NameValueCollection Headers + public HttpHeaderCollection Headers { get { @@ -153,7 +153,7 @@ public NameValueCollection Headers { if (contextServerConfiguration.Flags.NormalizeHeadersEncodings) { - headers = new NameValueCollection(); + headers = new HttpHeaderCollection(); Encoding entryCodepage = Encoding.GetEncoding("ISO-8859-1"); foreach (string headerName in listenerRequest.Headers) { @@ -166,7 +166,7 @@ public NameValueCollection Headers } else { - headers = listenerRequest.Headers; + headers = new HttpHeaderCollection(listenerRequest.Headers); } } @@ -708,7 +708,7 @@ public HttpResponse Close() /// cached in this object. /// /// - /// public Stream GetInputStream() + /// public Stream GetRequestStream() /// /// /// Method diff --git a/src/Http/HttpResponse.cs b/src/Http/HttpResponse.cs index 3546cf6..7532289 100644 --- a/src/Http/HttpResponse.cs +++ b/src/Http/HttpResponse.cs @@ -19,7 +19,7 @@ namespace Sisk.Core.Http /// /// /// public class HttpResponse : CookieHelper - /// + /// /// /// Class /// @@ -84,16 +84,15 @@ public static HttpResponse CreateRedirectResponse(RouteAction action) } /// - /// Gets or sets an custom HTTP status code and description for this HTTP response. If this property ins't null, it will overwrite - /// the property in this class. + /// Gets or sets the HTTP status code and description for this HTTP response. /// /// - /// public HttpStatusInformation? CustomStatus { get; set; } + /// public HttpStatusInformation StatusInformation { get; set; } /// /// /// Property /// - public HttpStatusInformation? CustomStatus { get; set; } = null; + public HttpStatusInformation StatusInformation { get; set; } = new HttpStatusInformation(); /// /// Gets or sets the HTTP response status code. @@ -104,7 +103,7 @@ public static HttpResponse CreateRedirectResponse(RouteAction action) /// /// Property /// - public HttpStatusCode Status { get; set; } = HttpStatusCode.OK; + public HttpStatusCode Status { get => (HttpStatusCode)StatusInformation.StatusCode; set => StatusInformation = new HttpStatusInformation(value); } /// /// Gets a instance of the HTTP response headers. @@ -146,12 +145,6 @@ internal HttpResponse(byte internalStatus) this.internalStatus = internalStatus; } - internal HttpResponse(HttpListenerResponse res) - { - Status = (HttpStatusCode)res.StatusCode; - Headers.Add(res.Headers); - } - /// /// Gets the raw HTTP response message. /// @@ -166,16 +159,23 @@ internal HttpResponse(HttpListenerResponse res) public string GetRawHttpResponse(bool includeBody = true) { StringBuilder sb = new StringBuilder(); - sb.AppendLine($"HTTP/1.1 {(int)Status}"); + sb.AppendLine($"HTTP/1.1 {StatusInformation}"); foreach (string header in Headers) { sb.Append(header + ": "); sb.Append(Headers[header]); sb.Append('\n'); } + if (Content?.Headers is not null) + foreach (var header in Content.Headers) + { + sb.Append(header.Key + ": "); + sb.Append(string.Join(", ", header.Value)); + sb.Append('\n'); + } sb.Append('\n'); - if (includeBody) + if (includeBody && Content is not StreamContent) { sb.Append(Content?.ReadAsStringAsync().Result); } @@ -283,8 +283,7 @@ public HttpResponse WithStatus(int status) /// public HttpResponse WithStatus(HttpStatusInformation statusInformation) { - Status = default; - CustomStatus = statusInformation; + StatusInformation = statusInformation; return this; } @@ -340,8 +339,6 @@ public HttpResponse WithCookie(string name, string value, DateTime? expires = nu /// public HttpResponse() { - Status = HttpStatusCode.OK; - Content = null; } /// @@ -388,6 +385,18 @@ public HttpResponse(int status, HttpContent? content) : this((HttpStatusCode)sta /// public HttpResponse(HttpContent? content) : this(HttpStatusCode.OK, content) { } + /// + /// Creates an new instanec with given string content and status code as 200 OK. + /// + /// The UTF-8 string content. + /// + /// public HttpResponse(string stringContent) + /// + /// + /// Constructor + /// + public HttpResponse(string stringContent) : this(HttpStatusCode.OK, new StringContent(stringContent)) { } + /// /// Creates an new instance with given status code and HTTP contents. /// @@ -403,18 +412,6 @@ public HttpResponse(HttpStatusCode status, HttpContent? content) Content = content; } - internal string? GetHeader(string headerName) - { - foreach (string header in Headers.Keys) - { - if (string.Compare(header, headerName, StringComparison.OrdinalIgnoreCase) == 0) - { - return Headers[header]; - } - } - return null; - } - /// protected override void SetCookieHeader(String name, String value) { diff --git a/src/Http/HttpServer.cs b/src/Http/HttpServer.cs index 1bcdc16..e06a473 100644 --- a/src/Http/HttpServer.cs +++ b/src/Http/HttpServer.cs @@ -57,7 +57,7 @@ public partial class HttpServer : IDisposable internal HttpEventSourceCollection _eventCollection = new HttpEventSourceCollection(); internal HttpWebSocketConnectionCollection _wsCollection = new HttpWebSocketConnectionCollection(); internal List? listeningPrefixes; - internal HttpServerHandlerRepository handler = new HttpServerHandlerRepository(); + internal HttpServerHandlerRepository handler; static HttpServer() { @@ -257,6 +257,7 @@ public HttpServer(HttpServerConfiguration configuration) { _listenerCallback = new AsyncCallback(ListenerCallback); ServerConfiguration = configuration; + handler = new HttpServerHandlerRepository(this); handler.RegisterHandler(new DefaultHttpServerHandler()); } diff --git a/src/Http/HttpServerConfiguration.cs b/src/Http/HttpServerConfiguration.cs index 3c93b63..ef5ed3c 100644 --- a/src/Http/HttpServerConfiguration.cs +++ b/src/Http/HttpServerConfiguration.cs @@ -57,11 +57,8 @@ public class HttpServerConfiguration : IDisposable public CultureInfo? DefaultCultureInfo { get; set; } /// - /// Gets or sets the object which the HTTP server will write HTTP server access messages to. + /// Gets or sets the object which the HTTP server will write HTTP server access messages to. /// - /// - /// This property defaults to Console.Out. - /// /// /// public TextWriter? AccessLogsStream { get; set; } /// @@ -71,11 +68,8 @@ public class HttpServerConfiguration : IDisposable public LogStream? AccessLogsStream { get; set; } = null; /// - /// Gets or sets the object which the HTTP server will write HTTP server error transcriptions to. + /// Gets or sets the object which the HTTP server will write HTTP server error transcriptions to. /// - /// - /// This stream can be empty if ThrowExceptions is true. - /// /// /// public TextWriter? ErrorsLogsStream { get; set; } /// diff --git a/src/Http/HttpServer__Core.cs b/src/Http/HttpServer__Core.cs index 1b96fe5..fd8fa1b 100644 --- a/src/Http/HttpServer__Core.cs +++ b/src/Http/HttpServer__Core.cs @@ -280,16 +280,8 @@ private async Task ProcessRequest(HttpListenerContext context) goto finishSending; } - if (response.CustomStatus != null) - { - baseResponse.StatusCode = response.CustomStatus.Value.StatusCode; - baseResponse.StatusDescription = response.CustomStatus.Value.Description; - } - else - { - baseResponse.StatusCode = (int)response.Status; - } - + baseResponse.StatusCode = response.StatusInformation.StatusCode; + baseResponse.StatusDescription = response.StatusInformation.Description; baseResponse.KeepAlive = ServerConfiguration.KeepAlive; #endregion @@ -310,19 +302,39 @@ private async Task ProcessRequest(HttpListenerContext context) baseResponse.Headers[incameHeader] = value; } - if (response.Content != null) + if (response.Content is not null) { + Stream? contentStream = null; // determines the content type baseResponse.ContentType = resHeaders[HttpKnownHeaderNames.ContentType] ?? response.Content.Headers.ContentType?.ToString(); - // determines if the response should be sent as chunked or normal - if (response.SendChunked || responseContentLength == null) + // determines if the response should be sent as chunked or normal + if (response.SendChunked) { baseResponse.SendChunked = true; } + else if (responseContentLength is long contentLength) + { + baseResponse.ContentLength64 = contentLength; + } + else if (response.Content is StreamContent stmContent) + { + contentStream = await stmContent.ReadAsStreamAsync(); + if (!contentStream.CanSeek) + { + throw new InvalidOperationException(SR.HttpResponse_Stream_ContentLenghtNotSet); + } + else + { + contentStream.Position = 0; + baseResponse.ContentLength64 = contentStream.Length; + } + } else { - baseResponse.ContentLength64 = responseContentLength.Value; + // the content-length wasn't informed and the user didn't set the request to + // send as chunked. so it will throw an exception. + throw new InvalidOperationException(SR.HttpResponse_Stream_ContentLenghtNotSet); } // write the output buffer @@ -336,23 +348,8 @@ response.Content is StreamContent || if (isPayloadStreamable) { - Stream contentStream = await response.Content.ReadAsStreamAsync(); - - if (contentStream.CanSeek) - { - contentStream.Position = 0; - if (!baseResponse.SendChunked) - baseResponse.ContentLength64 = contentStream.Length; - } - else - { - if (baseResponse.ContentLength64 == 0) - { - // the content-length wasn't informed and the user didn't set the request to - // send as chunked. so it will throw an exception. - throw new InvalidOperationException(SR.HttpResponse_Stream_ContentLenghtNotSet); - } - } + if (contentStream is null) + contentStream = await response.Content.ReadAsStreamAsync(); await contentStream.CopyToAsync(baseResponse.OutputStream, flag.RequestStreamCopyBufferSize); } @@ -420,6 +417,7 @@ response.Content is StreamContent || { if (!ServerConfiguration.ThrowExceptions) { + baseResponse.StatusCode = 500; executionResult.ServerException = ex; executionResult.Status = HttpServerExecutionStatus.ExceptionThrown; } @@ -443,7 +441,7 @@ response.Content is StreamContent || catch (Exception) { baseResponse.Abort(); - } + } } if (OnConnectionClose != null) diff --git a/src/Http/HttpStatusInformation.cs b/src/Http/HttpStatusInformation.cs index c3c4521..2c1db21 100644 --- a/src/Http/HttpStatusInformation.cs +++ b/src/Http/HttpStatusInformation.cs @@ -8,6 +8,7 @@ // Repository: https://github.com/sisk-http/core using Sisk.Core.Internal; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.CompilerServices; @@ -17,15 +18,15 @@ namespace Sisk.Core.Http /// Represents a structure that holds an HTTP response status information, with its code and description. /// /// - /// public struct HttpStatusInformation + /// public struct HttpStatusInformation : IComparable, IEquatable{{HttpStatusInformation}} /// /// /// Struct /// - public struct HttpStatusInformation + public struct HttpStatusInformation : IComparable, IEquatable { - private int __statusCode = 100; - private string __description = "Continue"; + private int __statusCode; + private string __description; /// /// Gets or sets the short description of the HTTP message. @@ -53,6 +54,21 @@ public int StatusCode } } + /// + /// Creates an new with default parameters (200 OK) status. + /// + /// + /// public HttpStatusInformation() + /// + /// + /// Constructor + /// + public HttpStatusInformation() + { + __statusCode = 200; + __description = "OK"; + } + /// /// Creates an new instance with given parameters. /// @@ -66,8 +82,9 @@ public int StatusCode /// public HttpStatusInformation(int statusCode) { - StatusCode = statusCode; - Description = GetStatusCodeDescription(statusCode); + ValidateStatusCode(statusCode); + __statusCode = statusCode; + __description = GetStatusCodeDescription(statusCode); } /// @@ -82,8 +99,10 @@ public HttpStatusInformation(int statusCode) /// public HttpStatusInformation(HttpStatusCode statusCode) { - StatusCode = (int)statusCode; - Description = GetStatusCodeDescription(StatusCode); + int s = (int)statusCode; + ValidateStatusCode(s); + __statusCode = s; + __description = GetStatusCodeDescription(statusCode); } /// @@ -100,16 +119,17 @@ public HttpStatusInformation(HttpStatusCode statusCode) /// public HttpStatusInformation(int statusCode, string description) { - Description = description ?? throw new ArgumentNullException(nameof(description)); - StatusCode = statusCode; ValidateStatusCode(statusCode); ValidateDescription(description); + __statusCode = statusCode; + __description = description; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ValidateStatusCode(int st) { - if (st < 100 || st > 999) throw new ProtocolViolationException(SR.HttpStatusCode_IllegalStatusCode); + if (st < 100 || st > 999) + throw new ProtocolViolationException(SR.HttpStatusCode_IllegalStatusCode); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -133,5 +153,94 @@ public static string GetStatusCodeDescription(int statusCode) ValidateStatusCode(statusCode); return HttpStatusDescription.Get(statusCode); } + + /// + /// Gets the description of the HTTP status based on its description. + /// + /// The HTTP status code. + /// + /// public static string GetStatusCodeDescription(HttpStatusCode statusCode) + /// + /// + /// Static method + /// + public static string GetStatusCodeDescription(HttpStatusCode statusCode) + { + return GetStatusCodeDescription((int)statusCode); + } + + /// + /// Gets an corresponding to this instance, or null if the HTTP status does not match any value. + /// + /// + /// An or null if the HTTP status matches no entry on it. + /// + /// + /// public HttpStatusCode? GetHttpStatusCode() + /// + /// + /// Static method + /// + public HttpStatusCode? GetHttpStatusCode() + { + HttpStatusCode s = (HttpStatusCode)__statusCode; + if (Enum.IsDefined(s)) + { + return s; + } + return null; + } + + /// + /// + public readonly bool Equals(HttpStatusInformation other) + { + return other.__statusCode.Equals(this.__statusCode) && other.__description.Equals(this.__description); + } + + /// + /// + public int CompareTo(object? obj) + { + if (obj is HttpStatusInformation other) + { + if (other.__statusCode == this.__statusCode) return 0; + if (other.__statusCode > this.__statusCode) return 1; + if (other.__statusCode < this.__statusCode) return -1; + } + return -1; + } + + /// + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + if (obj is HttpStatusInformation other) + { + return Equals(other); + } + return false; + } + + /// + /// + public override int GetHashCode() + { + return this.__statusCode.GetHashCode() ^ this.__statusCode.GetHashCode(); + } + + /// + /// Gets an string representation of this HTTP Status Code. + /// + /// + /// public string ToString() + /// + /// + /// Method + /// + public override string ToString() + { + return $"{__statusCode} {__description}"; + } } } diff --git a/src/Http/LogStream.cs b/src/Http/LogStream.cs index c2300a1..3353fc4 100644 --- a/src/Http/LogStream.cs +++ b/src/Http/LogStream.cs @@ -40,7 +40,7 @@ public class LogStream : IDisposable /// /// Field /// - public static LogStream ConsoleOutput = new LogStream(Console.Out); + public static readonly LogStream ConsoleOutput = new LogStream(Console.Out); /// /// Gets the defined for this . @@ -325,7 +325,7 @@ private void ProcessQueue() if (_bufferingContent is not null) { - _bufferingContent.PushBack(exitBuffer.ToString()); + _bufferingContent.Add(exitBuffer.ToString()); } // writes log to outputs @@ -366,7 +366,7 @@ public virtual void WriteException(Exception exp) { StringBuilder excpStr = new StringBuilder(); WriteExceptionInternal(excpStr, exp, 0); - WriteLine(excpStr.ToString(), Array.Empty()); + WriteLineInternal(excpStr.ToString()); } /// @@ -380,7 +380,7 @@ public virtual void WriteException(Exception exp) /// public void WriteLine() { - WriteLine("", Array.Empty()); + WriteLineInternal(""); } /// @@ -412,7 +412,7 @@ public void WriteFormat(object? input) /// public void WriteLine(object? message) { - WriteLine(message?.ToString() ?? "", Array.Empty()); + WriteLineInternal(message?.ToString() ?? ""); } /// @@ -427,7 +427,7 @@ public void WriteLine(object? message) /// public void WriteLine(string message) { - WriteLine(message, Array.Empty()); + WriteLineInternal(message); } /// @@ -436,35 +436,23 @@ public void WriteLine(string message) /// The string format that represents the arguments positions. /// An array of objects that represents the string format slots values. /// - /// public virtual void WriteLine(string format, params object?[] args) + /// public void WriteLine(string format, params object?[] args) /// /// /// Method /// - public virtual void WriteLine(string format, params object?[] args) + public void WriteLine(string format, params object?[] args) { - EnqueueMessageLine(string.Format(format, args)); + WriteLineInternal(string.Format(format, args)); } /// - /// Represents the method that enqueues a message to the queue of messages to be written to output streams. + /// Represents the method that intercepts the line that will be written to an output log before being queued for writing. /// - /// The message which will be written. - /// Thrown when the message is null. - /// - /// protected void EnqueueMessage(string message) - /// - /// - /// Method - /// - protected void EnqueueMessageLine(string message) + /// The line which will be written to the log stream. + protected virtual void WriteLineInternal(string line) { - ArgumentNullException.ThrowIfNull(message, nameof(message)); - lock (logQueue) - { - logQueue.Enqueue(message); - setWatcher(); - } + EnqueueMessageLine(line.Normalize()); } /// @@ -523,6 +511,16 @@ public LogStream ConfigureRotatingPolicy(long maximumSize, TimeSpan dueTime) return this; } + void EnqueueMessageLine(string message) + { + ArgumentNullException.ThrowIfNull(message, nameof(message)); + lock (logQueue) + { + logQueue.Enqueue(message); + setWatcher(); + } + } + void WriteExceptionInternal(StringBuilder exceptionSbuilder, Exception exp, int currentDepth = 0) { if (currentDepth == 0) diff --git a/src/Internal/SR.cs b/src/Internal/SR.cs index b13d9f8..a3ab128 100644 --- a/src/Internal/SR.cs +++ b/src/Internal/SR.cs @@ -23,7 +23,7 @@ static class SR public const string HttpRequest_AlreadyInStreamingState = "This HTTP request is already streaming another context."; public const string HttpResponse_Redirect_NotMatchGet = "The specified method does not handle GET requests."; - public const string HttpResponse_Stream_ContentLenghtNotSet = "To write content to the response output stream, you need to specify the length of the content with SetContentLength or use a chunked-encoded transfer."; + public const string HttpResponse_Stream_ContentLenghtNotSet = "The response has content, but the content length was not defined. To send content without a specified length, enable HttpResponse.SendChunked."; public const string Httpserver_NoListeningHost = "Cannot start the HTTP server with no listening hosts."; public const string Httpserver_StartMessage = "The HTTP server is listening at:"; diff --git a/src/Routing/RequestCallback.cs b/src/Routing/RequestCallback.cs index 93225e5..533a60e 100644 --- a/src/Routing/RequestCallback.cs +++ b/src/Routing/RequestCallback.cs @@ -36,6 +36,18 @@ namespace Sisk.Core.Routing /// public delegate HttpResponse NoMatchedRouteErrorCallback(); + /// + /// Represents the function that is called when an request reaches it's route path, but it's + /// method is incorrect. + /// + /// + /// public delegate HttpResponse MethodNotAllowedErrorCallback(HttpContext context); + /// + /// + /// Delegate + /// + public delegate HttpResponse MethodNotAllowedErrorCallback(HttpContext context); + /// /// Represents the function that is called after the route action threw an exception. /// diff --git a/src/Routing/RouteMethod.cs b/src/Routing/RouteMethod.cs index 9643eab..4439825 100644 --- a/src/Routing/RouteMethod.cs +++ b/src/Routing/RouteMethod.cs @@ -87,7 +87,7 @@ public enum RouteMethod : int /// /// Enum Value /// - [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.CustomStatus or RouterMethod.Any property for routes." + + [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.StatusInformation or RouterMethod.Any property for routes." + " This enum value will be removed in a future Sisk update.")] Copy = 2 << 5, @@ -122,7 +122,7 @@ public enum RouteMethod : int /// /// Enum Value /// - [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.CustomStatus or RouterMethod.Any property for routes." + + [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.StatusInformation or RouterMethod.Any property for routes." + " This enum value will be removed in a future Sisk update.")] Link = 2 << 8, @@ -135,7 +135,7 @@ public enum RouteMethod : int /// /// Enum Value /// - [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.CustomStatus or RouterMethod.Any property for routes." + + [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.StatusInformation or RouterMethod.Any property for routes." + " This enum value will be removed in a future Sisk update.")] Unlink = 2 << 9, @@ -148,7 +148,7 @@ public enum RouteMethod : int /// /// Enum Value /// - [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.CustomStatus or RouterMethod.Any property for routes." + + [Obsolete("This HTTP method is not defined in RFC 7231, Session 4, making it specific to a specific technology. To use custom HTTP methods, consider using the HttpResponse.StatusInformation or RouterMethod.Any property for routes." + " This enum value will be removed in a future Sisk update.")] View = 2 << 10, diff --git a/src/Routing/Router.cs b/src/Routing/Router.cs index 6346c47..72a3d9e 100644 --- a/src/Routing/Router.cs +++ b/src/Routing/Router.cs @@ -9,6 +9,7 @@ using Sisk.Core.Http; using Sisk.Core.Internal; +using System.Collections; using System.Collections.Immutable; using System.Runtime.CompilerServices; @@ -18,12 +19,12 @@ namespace Sisk.Core.Routing /// Represents a collection of Routes and main executor of callbacks in an . /// /// - /// public class Router + /// public class Router : IEnumerable{{Route}} /// /// /// Class /// - public partial class Router + public partial class Router : IEnumerable { internal record RouterExecutionResult(HttpResponse? Response, Route? Route, RouteMatchResult Result, Exception? Exception); @@ -128,8 +129,8 @@ public Router() /// /// Property /// - public NoMatchedRouteErrorCallback? MethodNotAllowedErrorHandler { get; set; } = new NoMatchedRouteErrorCallback( - () => new HttpResponse(System.Net.HttpStatusCode.MethodNotAllowed)); + public MethodNotAllowedErrorCallback? MethodNotAllowedErrorHandler { get; set; } = new MethodNotAllowedErrorCallback( + (c) => new HttpResponse(System.Net.HttpStatusCode.MethodNotAllowed)); /// /// Gets all routes defined on this router instance. @@ -182,6 +183,20 @@ HttpResponse ResolveAction(object routeResult) return actionHandler(routeResult); } + + /// + /// + public IEnumerator GetEnumerator() + { + return _routes.GetEnumerator(); + } + + /// + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_routes).GetEnumerator(); + } } internal enum RouteMatchResult diff --git a/src/Routing/Router__CoreInvoker.cs b/src/Routing/Router__CoreInvoker.cs index ce22c14..85ff178 100644 --- a/src/Routing/Router__CoreInvoker.cs +++ b/src/Routing/Router__CoreInvoker.cs @@ -184,7 +184,8 @@ internal async Task Execute(HttpContext context) } else if (matchResult == RouteMatchResult.PathMatched && MethodNotAllowedErrorHandler is not null) { - return new RouterExecutionResult(MethodNotAllowedErrorHandler(), matchedRoute, matchResult, null); + context.MatchedRoute = matchedRoute; + return new RouterExecutionResult(MethodNotAllowedErrorHandler(context), matchedRoute, matchResult, null); } else if (matchResult == RouteMatchResult.FullyMatched && matchedRoute != null) { diff --git a/src/Routing/Router__CoreSetters.cs b/src/Routing/Router__CoreSetters.cs index 5b49090..679f5d4 100644 --- a/src/Routing/Router__CoreSetters.cs +++ b/src/Routing/Router__CoreSetters.cs @@ -242,7 +242,8 @@ public void SetRoute(Route r) } /// - /// Searches the object instance for methods with attribute and optionals , and creates routes from them. + /// Searches in the specified object for instance methods marked with routing attributes, such as and optionals , and creates + /// routes from them. All routes is delegated to the specified instance. /// /// The instance of the class where the instance methods are. The routing methods must be instance methods and marked with . /// An exception is thrown when a method has an erroneous signature. @@ -260,7 +261,8 @@ public void SetObject(object attrClassInstance) } /// - /// Searches the object for static methods with attribute and optionals , and creates routes from them. + /// Searches in the specified object for static methods marked with routing attributes, such as and optionals , and creates + /// routes from them. /// /// The type of the class where the static methods are. The routing methods must be static and marked with . /// An exception is thrown when a method has an erroneous signature. @@ -276,6 +278,26 @@ public void SetObject([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes SetInternal(methods, attrClassType, null); } + /// + /// Searches in the specified object for static methods marked with routing attributes, such as and optionals , and creates + /// routes from them. + /// + /// + /// This method is an shortcut for . + /// + /// The type of the class where the static methods are. The routing methods must be static and marked with . + /// An exception is thrown when a method has an erroneous signature. + /// + /// public void SetObject{{TObject}}() + /// + /// + /// Method + /// + public void SetObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TObject>() + { + SetObject(typeof(TObject)); + } + private void SetInternal(MethodInfo[] methods, Type callerType, object? instance) { RouterModule? rmodule = instance as RouterModule;