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 NejCommon.Utils; 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, IScopedService 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; } private static string? GetValue(ModelBindingContext context, string name) { // Route first if (context.ActionContext.RouteData.Values.TryGetValue(name, out var routeValue)) return routeValue?.ToString(); // Then query string if (context.HttpContext.Request.Query.TryGetValue(name, out var queryValue)) return queryValue.FirstOrDefault(); // Then form if (context.HttpContext.Request.HasFormContentType && context.HttpContext.Request.Form.TryGetValue(name, out var formValue)) return formValue.FirstOrDefault(); return null; } public async Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } using var activity = Telemetry.Start(); var cancellationToken = bindingContext.HttpContext.RequestAborted; cancellationToken.ThrowIfCancellationRequested(); //get the binding parameter attributes // Fetch the route values from the route data var values = new string[_routeValues.Length]; for (var i = 0; i < _routeValues.Length; i++) { var routeValue = _routeValues[i]; var name = routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName) ? bindingContext.ModelName : routeValue.Name; var result = GetValue(bindingContext, name); // If these really are route values, don't go through the entire // MVC ValueProvider infrastructure. if (result == null) { bindingContext.ModelState.AddModelError(name, "Route value not found"); bindingContext.Result = ModelBindingResult.Failed(); return; } values[i] = result; } // Check if all the required route values are present if (!bindingContext.ModelState.IsValid) { bindingContext.Result = ModelBindingResult.Failed(); return; } var entityParameter = Expression.Parameter(typeof(TEntity), "entity"); // Find the entity with the specified route values Expression? predicateBody = null; for (var i = 0; i < _routeValues.Length; i++) { var routeValue = _routeValues[i]; // IMPORTANT: // captured variable -> EF parameter // rather than Expression.Constant(value) var value = values[i]; Expression> valueExpression = () => value; var expression = Expression.Invoke( routeValue.Expression, entityParameter, valueExpression.Body); var body = expression.Reduce(); predicateBody = predicateBody is null ? body : Expression.AndAlso(predicateBody, body); } var predicate = Expression.Lambda>( predicateBody!, entityParameter); var model = await _db.Set().AsQueryable().FirstOrDefaultAsync(predicate, cancellationToken); 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; } }