Навигация-маршрутизация OData для универсального контроллера с поддержкой ODataQueryOptions - PullRequest
0 голосов
/ 08 ноября 2018

У меня есть один универсальный контроллер (похожий на этот: .Net Core Override Controller Route для универсального контроллера ), который регистрирует универсальные реализации для всех динамических типов, которые у меня есть.

Это работает очень хорошо. Но при попытке реализовать поддержку навигационной маршрутизации с дополнительными значениями фильтра у меня возникают некоторые проблемы. Этот пример:

http://localhost/odata/EntityA(4711)/SubEntity?$filter=category экв 'ABC'

работает теоретически, но мне нужно извлечь ODataQueryOptions.

Итак, вот что у меня есть:

ExternalControllerFeatureProvider

public class ExternalControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
    public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
    {
        foreach (var candidate in _entityCompiler.GetTypes())
        {
            feature.Controllers.Add(
                typeof(GenericController<>).MakeGenericType(candidate).GetTypeInfo()
            );
        }
    }
}

GenericController

[Produces("application/json")]
[GenericControllerNameConvention]
[EnableQuery]
public class GenericController<T> : ODataController
{
    public async Task<IQueryable<T>> Get([FromServices] ODataQueryOptions odataQueryOptions)
    {
        var parameters = ExtractQueryParameter(odataQueryOptions);

        return await InternalGet(parameters);
    }

    public async Task<IQueryable<T>> Get([FromServices] ODataQueryOptions odataQueryOptions, [FromODataUri] object key)
    {
        var parameters = ExtractQueryParameter(odataQueryOptions);
        AppendKeyAttributeFilter(parameters, key);

        return await InternalGet(parameters);
    }

    public async Task<IActionResult> GetNavigation(Guid key, string propertyName)
    {
        var parameters = new Dictionary<string, object>();
        AppendKeyAttributeFilter(parameters, key);
        AppendExpandFilter(parameters, propertyName);

        var rootObject = await InternalGet(parameters);

        if (rootObject.Any())
        {
            var info = typeof(T).GetProperty(propertyName);

            object value = info.GetValue(rootObject.FirstOrDefault());

            return Ok(value);
        }

        return NotFound();
    }

Аналогично этому (http://odata.github.io/WebApi/03-04-custom-routing-convention/) Я создал NavigationRoutingConvention , который извлекает свойство навигации и вызывает GetNavigation -метод из GenericController с правильным propertyName .

Проблема в том, что этот метод GenericController не может возвращать IQueryable или IEnumerable , но только некоторые нетипизированные типы, такие как IActionResult .

Чтобы вручную отфильтровать источник данных в бэкэнде, мне нужны ODataQueryOptions , как в обоих методах Get. Проблема в том, что, похоже, базовая структура должна знать правильный возвращаемый тип.

Если я добавлю [FromServices] ODataQueryOptions к заголовку метода, я получу следующее исключение:

System.InvalidOperationException: невозможно создать модель EDM как действие «GetNavigation» на контроллере «EntityA» имеет тип возврата «System.Threading.Tasks.Task`1 [[Microsoft.AspNetCore.Mvc.IActionResult, Microsoft.AspNetCore.Mvc.Abstractions, версия = 2.1.1.0, Культура = нейтральная, PublicKeyToken = adb9793829ddae60]] ', которая не реализовать IEnumerable. в Microsoft.AspNet.OData.ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType (ActionDescriptor actionDescriptor) в Microsoft.AspNet.OData.ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.BindModelAsync (ModelBindingContext bindingContext) в Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BinderTypeModelBinder.BindModelAsync (ModelBindingContext bindingContext) в Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync (ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, Параметр ParameterDescriptor, метаданные ModelMetadata, значение объекта)
в . Microsoft.AspNetCore.Mvc.Internal.ControllerBinderDelegateProvider <> c__DisplayClass0_0 . D.MoveNext ()

1 Ответ

0 голосов
/ 12 ноября 2018

Итак, я нашел решение. Я воздержался от идеи собственного соглашения о маршрутизации и добавил еще один универсальный контроллер, особенно для свойств суб-навигации. Ниже аннотации не работает код, очищенный некоторыми частными частями ...: -)

GenericSubNavigationController

[Produces("application/json")]
[GenericControllerNameConvention]
[EnableQuery]
public class GenericSubNavigationController<TBaseType, TSubType, TSubTypeDeclared> : GenericControllerBase<TBaseType>
{
    public GenericSubNavigationController(ISubTypeEnricher subTypeEnricher) : base(subTypeEnricher)
    {
    }

    public async Task<IQueryable<TSubTypeDeclared>> GetNavigation([FromServices] ODataQueryOptions odataQueryOptions, Guid key)
    {
        PropertyInfo propertyInfo = typeof(TBaseType).GetProperties().FirstOrDefault(x => x.PropertyType == typeof(TSubType));

        string propertyName = propertyInfo.Name;

        var parameters = new Dictionary<string, string>();
        AppendKeyAttributeFilter(parameters, key);
        AppendExpandFilter(parameters, propertyName);

        var subParameters = new Tuple<string, Dictionary<string, string>>(propertyName, ExtractQueryParameter(odataQueryOptions));

        var rootObject = await InternalGet<TBaseType>(parameters, subParameters);

        if (rootObject.Any())
        {
            var info = typeof(TBaseType).GetProperty(propertyName);

            object value = info.GetValue(rootObject.FirstOrDefault());

            return new EnumerableQuery<TSubTypeDeclared>((IEnumerable<TSubTypeDeclared>) value);
        }

        return null;
    }
}

Чтобы работать, вы должны создать экземпляр этого контроллера в ExternalControllerFeatureProvider , который уже упоминался в моем первоначальном вопросе

ExternalControllerFeatureProvider

public class ExternalControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
    private readonly IExternalCompiler _entityCompiler;

    public ExternalControllerFeatureProvider(IExternalCompiler entityCompiler)
    {
        _entityCompiler = entityCompiler;
    }

    public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
    {
        var types = _entityCompiler.GetTypes().ToList();
        foreach (var candidate in types)
        {
            feature.Controllers.Add(
                typeof(GenericController<>).MakeGenericType(candidate).GetTypeInfo()
            );

            foreach (var propertyInfo in candidate.GetProperties())
            {
                Type targetType = propertyInfo.PropertyType.GenericTypeArguments.Any()
                    ? propertyInfo.PropertyType.GenericTypeArguments.First()
                    : propertyInfo.PropertyType;
                if (types.Contains(targetType))
                {
                    var typeInfo = typeof(GenericSubNavigationController<,,>).MakeGenericType(candidate, propertyInfo.PropertyType, targetType).GetTypeInfo();
                    feature.Controllers.Add(typeInfo);
                }
            }
        }
    }
}

И, наконец, мы должны изменить используемый атрибут GenericControllerNameConvention , чтобы изменить имя действия методов для отражения требований OData по умолчанию

GenericControllerNameConvention

public class GenericControllerNameConvention : Attribute, IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        if (!controller.ControllerType.IsGenericType || (controller.ControllerType.GetGenericTypeDefinition() !=
            typeof(GenericController<>) && controller.ControllerType.GetGenericTypeDefinition() !=
            typeof(GenericSubNavigationController<,,>)))
        {
            // Not a GenericController, ignore.
            return;
        }

        var entityType = controller.ControllerType.GenericTypeArguments[0];
        controller.ControllerName = $"{entityType.Name}";

        if (controller.ControllerType.GetGenericTypeDefinition() ==
            typeof(GenericSubNavigationController<,,>))
        {
            foreach (var controllerAction in controller.Actions)
            {
                if (controllerAction.ActionName == "GetNavigation")
                {
                    var subType = controller.ControllerType.GenericTypeArguments[1];
                    PropertyInfo propertyInfo = entityType.GetProperties().FirstOrDefault(x => x.PropertyType == subType);

                    controllerAction.ActionName = $"Get{propertyInfo.Name}";
                }
            }
        }
    }
}
...