Extend and generalize data binding logic (#14)

Support for custom data binding attributes
This commit is contained in:
2022-02-26 10:37:32 +01:00
committed by GitHub
parent 85a90492c6
commit 4aa1150bba
14 changed files with 169 additions and 238 deletions
@@ -1,8 +1,12 @@
using System;
using MTSC.Common.Http.RoutingModules;
namespace MTSC.Common.Http.Attributes
{
public sealed class FromBodyAttribute : Attribute
public sealed class FromBodyAttribute : StringRouteDataBindingBaseAttribute
{
public override string GetStringValue(HttpRouteBase route, RouteContext routeContext)
{
return routeContext.HttpRequest.BodyString;
}
}
}
@@ -1,8 +1,8 @@
using System;
using MTSC.Common.Http.RoutingModules;
namespace MTSC.Common.Http.Attributes
{
public sealed class FromHeadersAttribute : Attribute
public sealed class FromHeadersAttribute : StringRouteDataBindingBaseAttribute
{
public string HeaderName { get; }
@@ -10,5 +10,15 @@ namespace MTSC.Common.Http.Attributes
{
this.HeaderName = headerName;
}
public override string GetStringValue(HttpRouteBase route, RouteContext routeContext)
{
if (routeContext.HttpRequest.Headers.ContainsHeader(this.HeaderName))
{
return routeContext.HttpRequest.Headers[this.HeaderName];
}
return null;
}
}
}
@@ -1,8 +1,9 @@
using System;
using MTSC.Common.Http.RoutingModules;
using System;
namespace MTSC.Common.Http.Attributes
{
public sealed class FromRouteContextResourcesAttribute : Attribute
public sealed class FromRouteContextResourcesAttribute : RouteDataBindingBaseAttribute
{
public string ResourceKey { get; }
@@ -10,5 +11,15 @@ namespace MTSC.Common.Http.Attributes
{
this.ResourceKey = resourceKey;
}
public override object DataBind(HttpRouteBase route, RouteContext routeContext, Type propertyType)
{
if (routeContext.Resources.TryGetValue(this.ResourceKey, out var resource))
{
return resource;
}
return null;
}
}
}
@@ -1,8 +1,8 @@
using System;
using MTSC.Common.Http.RoutingModules;
namespace MTSC.Common.Http.Attributes
{
public class FromUrlAttribute : Attribute
public class FromUrlAttribute : StringRouteDataBindingBaseAttribute
{
public string Placeholder { get; }
@@ -10,5 +10,15 @@ namespace MTSC.Common.Http.Attributes
{
this.Placeholder = placeholder;
}
public override string GetStringValue(HttpRouteBase route, RouteContext routeContext)
{
if (routeContext.UrlValues.TryGetValue(this.Placeholder, out var value))
{
return value;
}
return null;
}
}
}
@@ -0,0 +1,19 @@
using MTSC.Common.Http.RoutingModules;
using System;
namespace MTSC.Common.Http.Attributes
{
public abstract class RouteDataBindingBaseAttribute : Attribute
{
/// <summary>
/// Perform the data binding logic and return the bindable value.
/// This value will be attached to the decorated property.
/// The value can be
/// </summary>
/// <param name="module"><see cref="HttpRouteBase"/> object.</param>
/// <param name="routeContext"><see cref="RouteContext"/> for the current request.</param>
/// <param name="propertyType"><see cref="Type"/> of property to be binded.</param>
/// <returns>Object to be binded to the decorated property.</returns>
public abstract object DataBind(HttpRouteBase route, RouteContext routeContext, Type propertyType);
}
}
@@ -0,0 +1,63 @@
using MTSC.Common.Http.RoutingModules;
using Newtonsoft.Json;
using System;
using System.ComponentModel;
namespace MTSC.Common.Http.Attributes
{
public abstract class StringRouteDataBindingBaseAttribute : RouteDataBindingBaseAttribute
{
public override sealed object DataBind(HttpRouteBase route, RouteContext routeContext, Type propertyType)
{
var stringValue = this.GetStringValue(route, routeContext);
return BindValue(propertyType, stringValue);
}
/// <summary>
/// Returns a string value that will later be converted to the decorated property <see cref="Type"/>.
/// Conversion tries <see cref="TypeConverter"/> as well as <see cref="JsonConvert"/> if the property <see cref="Type"/> is not <see cref="string"/>.
/// </summary>
/// <param name="module"><see cref="HttpRouteBase"/> route.</param>
/// <param name="routeContext"><see cref="RouteContext"/> of the request.</param>
/// <returns>A string value to be converted to the decorated property <see cref="Type"/>.</returns>
public abstract string GetStringValue(HttpRouteBase route, RouteContext routeContext);
private object BindValue(Type propertyType, string value)
{
object finalValue = null;
if (propertyType == typeof(string))
{
finalValue = value;
}
else if (this.TryConvertWithTypeConverter(propertyType, value, out var typeConvertedValue))
{
finalValue = typeConvertedValue;
}
else if (this.TryConvertWithJsonConvert(propertyType, value, out var jsonConvertedValue))
{
finalValue = jsonConvertedValue;
}
return finalValue;
}
private bool TryConvertWithTypeConverter(Type propertyType, string value, out object convertedValue)
{
var typeConverter = TypeDescriptor.GetConverter(propertyType);
if (typeConverter.CanConvertFrom(typeof(string)))
{
convertedValue = typeConverter.ConvertFrom(value);
return true;
}
convertedValue = null;
return false;
}
private bool TryConvertWithJsonConvert(Type propertyType, string value, out object convertedValue)
{
convertedValue = JsonConvert.DeserializeObject(value, propertyType);
return true;
}
}
}
+5 -1
View File
@@ -9,18 +9,22 @@ namespace MTSC.Common.Http
public Server Server { get; }
public HttpRequest HttpRequest { get; }
public ClientData Client { get; }
public Dictionary<string, string> UrlValues { get; }
public HttpResponse HttpResponse { get; set; }
public CancellationToken CancelRequest => this.Client.CancellationToken;
public Dictionary<string, object> Resources { get; set; } = new();
public RouteContext(
Server server,
HttpRequest httpRequest,
ClientData clientData)
ClientData clientData,
Dictionary<string, string> urlValues)
{
this.Server = server;
this.HttpRequest = httpRequest;
this.Client = clientData;
this.UrlValues = urlValues;
}
}
}
+21 -14
View File
@@ -1,24 +1,31 @@
using System;
using MTSC.Common.Http;
using MTSC.Common.Http.Attributes;
using MTSC.Common.Http.RoutingModules;
using System;
using System.Reflection;
using System.Runtime.Serialization;
namespace MTSC.Exceptions
{
public abstract class DataBindingException : Exception
public sealed class DataBindingException : Exception
{
protected DataBindingException()
{
}
public HttpRouteBase Route { get; }
public RouteContext RouteContext { get; }
public RouteDataBindingBaseAttribute RouteDataBindingBaseAttribute { get; }
public PropertyInfo PropertyInfo { get; }
protected DataBindingException(string message) : base(message)
{
}
protected DataBindingException(string message, Exception innerException) : base(message, innerException)
{
}
protected DataBindingException(SerializationInfo info, StreamingContext context) : base(info, context)
public DataBindingException(
Exception innerException,
HttpRouteBase route,
RouteContext routeContext,
RouteDataBindingBaseAttribute routeDataBindingBaseAttribute,
PropertyInfo propertyInfo)
: base("Exception occurred during data binding. Check inner exception for details", innerException)
{
this.Route = route;
this.RouteContext = routeContext;
this.RouteDataBindingBaseAttribute = routeDataBindingBaseAttribute;
this.PropertyInfo = propertyInfo;
}
}
}
@@ -1,20 +0,0 @@
using System;
namespace MTSC.Exceptions
{
public sealed class FromBodyDataBindingException : Exception
{
public Type PropertyType { get; }
public string Value { get; }
public FromBodyDataBindingException(
Exception innerException,
Type propertyType,
string value)
: base("Encountered exception when binding data from body", innerException)
{
this.PropertyType = propertyType;
this.Value = value;
}
}
}
@@ -1,23 +0,0 @@
using System;
namespace MTSC.Exceptions
{
public sealed class FromHeadersDataBindingException : Exception
{
public Type PropertyType { get; }
public string Header { get; }
public string Value { get; }
public FromHeadersDataBindingException(
Exception innerException,
Type propertyType,
string header,
string value)
: base("Encountered exception when binding data from headers", innerException)
{
this.PropertyType = propertyType;
this.Header = header;
this.Value = value;
}
}
}
@@ -1,24 +0,0 @@
using System;
using MTSC.Common.Http;
namespace MTSC.Exceptions
{
public sealed class FromRouteContextResourcesDataBindingException : Exception
{
public Type PropertyType { get; }
public string Key { get; }
public RouteContext RouteContext { get; }
public FromRouteContextResourcesDataBindingException(
Exception innerException,
Type propertyType,
string key,
RouteContext routeContext)
: base("Encountered exception when binding data from route context resources", innerException)
{
this.PropertyType = propertyType;
this.Key = key;
this.RouteContext = routeContext;
}
}
}
@@ -1,23 +0,0 @@
using System;
namespace MTSC.Exceptions
{
public sealed class FromUrlDataBindingException : DataBindingException
{
public Type PropertyType { get; }
public string Placeholder { get; }
public string Value { get; }
public FromUrlDataBindingException(
Exception innerException,
Type propertyType,
string placeHolder,
string value)
: base("Encountered exception when binding data from url", innerException)
{
this.PropertyType = propertyType;
this.Placeholder = placeHolder;
this.Value = value;
}
}
}
+3 -3
View File
@@ -5,13 +5,13 @@
<TargetFrameworks>net48;netstandard2.0;netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<ApplicationIcon />
<StartupObject />
<Version>5.1.6</Version>
<Version>5.2.0</Version>
<LangVersion>latest</LangVersion>
<Authors>Alexandru-Victor Macocian</Authors>
<Product>MTSC</Product>
<Description>Modular TCP Server and Client</Description>
<AssemblyVersion>5.1.6.0</AssemblyVersion>
<FileVersion>5.1.6.0</FileVersion>
<AssemblyVersion>5.2.0.0</AssemblyVersion>
<FileVersion>5.2.0.0</FileVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Platforms>AnyCPU;x64</Platforms>
<PackageProjectUrl>https://github.com/AlexMacocian/MTSC</PackageProjectUrl>
+15 -122
View File
@@ -229,7 +229,11 @@ namespace MTSC.ServerSide.Handlers
httpLogger.LogRequest(server, this, client, request);
}
var routeContext = new RouteContext(server, request, client);
var routeContext = new RouteContext(
server,
request,
client,
urlValues.ToDictionary(u => u.Placeholder, u => u.Value));
foreach(var filterType in filterTypes)
{
var filter = server.ServiceManager.GetService(filterType) as RouteFilterAttribute;
@@ -254,7 +258,7 @@ namespace MTSC.ServerSide.Handlers
}
this.RouteHandleRequest(module, routeContext, filterTypes, urlValues).ContinueWith(task =>
this.RouteHandleRequest(module, routeContext, filterTypes).ContinueWith(task =>
{
this.QueueResponse(client, task.Result);
});
@@ -368,12 +372,11 @@ namespace MTSC.ServerSide.Handlers
private async Task<HttpResponse> RouteHandleRequest(
HttpRouteBase httpRouteBase,
RouteContext routeContext,
List<Type> filterTypes,
List<UrlValue> urlValues)
List<Type> filterTypes)
{
try
{
this.SetModuleProperties(httpRouteBase, routeContext, urlValues);
this.SetModuleProperties(httpRouteBase, routeContext);
var response = await httpRouteBase.CallHandleRequest(routeContext.HttpRequest);
routeContext.HttpResponse = response;
foreach (var filterType in filterTypes)
@@ -440,7 +443,7 @@ namespace MTSC.ServerSide.Handlers
{
foreach (var attribute in property.GetCustomAttributes(true))
{
if (attribute is FromUrlAttribute or FromBodyAttribute or FromHeadersAttribute)
if (attribute is RouteDataBindingBaseAttribute)
{
propertyAndAttributesList.Add(((Attribute)attribute, property));
}
@@ -470,99 +473,29 @@ namespace MTSC.ServerSide.Handlers
return false;
}
private void SetModuleProperties(HttpRouteBase module, RouteContext routeContext, List<UrlValue> urlValues)
private void SetModuleProperties(HttpRouteBase module, RouteContext routeContext)
{
var propertiesAndAttributes = this.routePropertyCache[module.GetType()];
foreach ((var attribute, var propertyInfo) in this.routePropertyCache[module.GetType()])
{
if (attribute is FromUrlAttribute fromUrlAttribute)
{
var maybeValue = urlValues.Where(val => val.Placeholder == fromUrlAttribute.Placeholder).FirstOrDefault();
if (maybeValue is not null)
{
try
{
this.TryAssignValue(propertyInfo, maybeValue.Value, module);
}
catch (Exception ex)
{
if (this.ThrowOnBindingErrors)
{
throw new FromUrlDataBindingException(ex, propertyInfo.PropertyType, maybeValue.Placeholder, maybeValue.Value);
}
}
}
}
else if (attribute is FromBodyAttribute)
if (attribute is RouteDataBindingBaseAttribute routeDataBindingBaseAttribute)
{
try
{
this.TryAssignValue(propertyInfo, routeContext.HttpRequest.BodyString, module);
var value = routeDataBindingBaseAttribute.DataBind(module, routeContext, propertyInfo.PropertyType);
SetPropertyValue(propertyInfo, value, module);
}
catch (Exception ex)
{
if (this.ThrowOnBindingErrors)
{
throw new FromBodyDataBindingException(ex, propertyInfo.PropertyType, routeContext.HttpRequest.BodyString);
}
}
}
else if (attribute is FromHeadersAttribute fromHeadersAttribute)
{
var maybeValue = routeContext.HttpRequest.Headers.Where(kvp => kvp.Key == fromHeadersAttribute.HeaderName).FirstOrDefault();
if (maybeValue.Value is not null)
{
try
{
this.TryAssignValue(propertyInfo, maybeValue.Value, module);
}
catch (Exception ex)
{
if (this.ThrowOnBindingErrors)
{
throw new FromHeadersDataBindingException(ex, propertyInfo.PropertyType, fromHeadersAttribute.HeaderName, maybeValue.Value);
}
}
}
}
else if (attribute is FromRouteContextResourcesAttribute fromRouteContextResourcesAttribute)
{
if (routeContext.Resources.TryGetValue(fromRouteContextResourcesAttribute.ResourceKey, out var value))
{
try
{
SetPropertyValue(propertyInfo, value, module);
}
catch (Exception ex)
{
if (this.ThrowOnBindingErrors)
{
throw new FromRouteContextResourcesDataBindingException(ex, propertyInfo.PropertyType, fromRouteContextResourcesAttribute.ResourceKey, routeContext);
}
throw new DataBindingException(ex, module, routeContext, routeDataBindingBaseAttribute, propertyInfo);
}
}
}
}
}
private void TryAssignValue(PropertyInfo propertyInfo, string value, HttpRouteBase module)
{
object finalValue = null;
if (propertyInfo.PropertyType == typeof(string))
{
finalValue = value;
}
else if (this.TryConvertWithTypeConverter(propertyInfo, value, out var typeConvertedValue))
{
finalValue = typeConvertedValue;
}
else if (this.TryConvertWithJsonConvert(propertyInfo, value, out var jsonConvertedValue))
{
finalValue = jsonConvertedValue;
}
SetPropertyValue(propertyInfo, finalValue, module);
}
private static void SetPropertyValue(PropertyInfo propertyInfo, object value, HttpRouteBase module)
{
@@ -577,47 +510,7 @@ namespace MTSC.ServerSide.Handlers
}
}
private bool TryConvertWithTypeConverter(PropertyInfo propertyInfo, string value, out object convertedValue)
{
try
{
var typeConverter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
if (typeConverter.CanConvertFrom(typeof(string)))
{
convertedValue = typeConverter.ConvertFrom(value);
return true;
}
}
catch
{
if (this.ThrowOnBindingErrors is true)
{
throw;
}
}
convertedValue = null;
return false;
}
private bool TryConvertWithJsonConvert(PropertyInfo propertyInfo, string value, out object convertedValue)
{
try
{
convertedValue = JsonConvert.DeserializeObject(value, propertyInfo.PropertyType);
return true;
}
catch
{
if (this.ThrowOnBindingErrors is true)
{
throw;
}
}
convertedValue = null;
return false;
}
private static HttpResponse NotFound404 => new()
{