using AngleSharp.Dom; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; namespace NejCommon.Models { public abstract class RouteValue { public string Name { get; } public bool Primary { get; } = false; public RouteValue(string name, bool primary = false) { Name = name; Primary = primary; } } public class RouteValue : RouteValue where TEntity : class { public Expression> Expression { get; } public RouteValue(string name, Expression> expression) : base(name) { Expression = expression; } public RouteValue(string name, bool primary, Expression> expression) : base(name, primary) { Expression = expression; } } public abstract class EntityBinder { } public abstract class EntityBinder : EntityBinder where TEntity : class { public EntityBinder(DbContext db, RouteValue[] routeValues) : base(db, routeValues) { } } //id should be Guid, int, string, etc. public abstract class EntityBinder : EntityBinder, IModelBinder where TEntity : class where TIdType : IEquatable { private readonly DbContext _db; private readonly RouteValue[] _routeValues; public RouteValue[] RouteValues => _routeValues; public EntityBinder(DbContext db, RouteValue[] routeValues) { _db = db; _routeValues = routeValues; } public async Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } //get the binding parameter attributes // Fetch the route values from the route data var capturedRouteData = new Dictionary(); foreach (var routeValue in _routeValues) { var name = routeValue.Name; if (routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName)) { name = bindingContext.ModelName; } /* Console.WriteLine("Route value: " + name); Console.WriteLine(); Console.WriteLine(bindingContext.ActionContext.RouteData.Values[name]); */ var value = bindingContext.ValueProvider.GetValue(name).FirstOrDefault();//bindingContext.ActionContext.RouteData.Values[name]; if (value is null) { bindingContext.ModelState.AddModelError(name, "Route value not found"); } else { capturedRouteData[routeValue.Name] = value; } } // Check if all the required route values are present if (!bindingContext.ModelState.IsValid) { bindingContext.Result = ModelBindingResult.Failed(); return; } // Find the entity with the specified route values var query = _db.Set().AsQueryable(); foreach (var routeValue in _routeValues) { var value = capturedRouteData[routeValue.Name]; var par = Expression.Parameter(typeof(TEntity)); var exp = Expression.Invoke(routeValue.Expression, par, Expression.Constant((string)value)); var redExp = exp.Reduce(); var compiledExp = Expression.Lambda>(redExp, par); query = query.Where(compiledExp); } var model = await query.FirstOrDefaultAsync(); if (model == null) { bindingContext.HttpContext.Response.StatusCode = 404; bindingContext.ModelState.AddModelError(bindingContext.FieldName, "Not found in DB"); bindingContext.Result = ModelBindingResult.Failed(); return; } bindingContext.Result = ModelBindingResult.Success(model); bindingContext.ValidationState[bindingContext.Result] = new ValidationStateEntry { SuppressValidation = true }; } } public class EntityBinderOperationFilter : IOperationFilter { public object CreateDummyInstance(Type entityBinderType) { var constructors = entityBinderType.GetConstructors(); var constructor = constructors.OrderBy(x => x.GetParameters().Count()).First(); var constructorParameters = constructor.GetParameters(); var entityBinder = (EntityBinder)constructor.Invoke(constructorParameters.Select(x => (object?)null).ToArray()); return entityBinder; } public void Apply(OpenApiOperation operation, OperationFilterContext context) { var actionDescriptor = context.ApiDescription.ActionDescriptor; // Get the action parameters with a ModelBinder attribute var modelBinderParameters = actionDescriptor.Parameters .Where(p => p.BindingInfo?.BinderType != null && typeof(EntityBinder).IsAssignableFrom(p.BindingInfo.BinderType)) .Where(x => x != null) .ToList(); if (modelBinderParameters.Count == 0) { return; } //Console.WriteLine("Applying EntityBinderOperationFilter to: " + actionDescriptor.DisplayName); operation.Parameters = operation.Parameters.Where(p => !modelBinderParameters.Any(mp => mp.Name == p.Name)).ToList(); /* // Get the EntityBinder RouteValues foreach (var parameter in modelBinderParameters) { var entityBinderType = parameter.BindingInfo!.BinderType; var routeValuesProperty = entityBinderType!.GetProperty("RouteValues"); if (routeValuesProperty == null) { continue; } var routeValues = (IEnumerable?)routeValuesProperty.GetValue(CreateDummyInstance(entityBinderType)); if (routeValues == null) { continue; } foreach (var routeValue in routeValues) { operation.Parameters.Add(new OpenApiParameter { Name = routeValue.Name, In = ParameterLocation.Path, Required = true, Schema = new OpenApiSchema { Type = "string" } }); } }*/ // Exclude the EntityBinder parameter types from the document schemas var schemaRep = context.SchemaRepository; foreach (var parameter in modelBinderParameters) { var entityBinderType = parameter.BindingInfo?.BinderType; // Get the EntityBinder generic type argument var entityType = GetBinderEntityType(entityBinderType); if (entityType == null) { Console.WriteLine("Couldn't find entityType of: " + entityBinderType); continue; } if (schemaRep.Schemas.ContainsKey(entityType.Name)) { //Console.WriteLine("Removing schema: " + entityType.Name); schemaRep.Schemas.Remove(entityType.Name); } } } private Type? GetBinderEntityType(Type? binderType) { if (binderType == null) return null; if (binderType.IsGenericType && binderType.GetGenericTypeDefinition() == typeof(EntityBinder<>)) { return binderType.GetTypeInfo().GetGenericArguments().FirstOrDefault(); } return GetBinderEntityType(binderType.BaseType); } } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public sealed class EntityAttribute : Attribute, IPropertyValidationFilter, IApiDescriptionVisibilityProvider, IBinderTypeProviderMetadata where TEntityBinder : EntityBinder where TEntity : class { /// public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) { return false; } /// public Type? BinderType => typeof(TEntityBinder); /// public BindingSource? BindingSource => BindingSource.Custom; public bool IgnoreApi => true; } }