diff options
author | Alex James <Alex@base4.net> | 2012-09-28 21:13:30 +0400 |
---|---|---|
committer | Alex James <Alex@base4.net> | 2012-10-08 22:58:10 +0400 |
commit | 7472815eec705c47a55a7a9c7e4f18545bf5a063 (patch) | |
tree | 3e8eef4ad77548dce03831a1108015118d700dc7 | |
parent | c1b5a3ef6b14685713da1e35b806f2c77a684dce (diff) |
Support for Transient bindable actions in the ODataModelBuilder Support for ActionLinkGeneration Support for by Convention ActionLinkGeneration Support efficient bound action lookups via a cache annotating the IEdmModel Support advertising ODataActions in Entity resources Refactoring ActionLinkBuilderAnnotation so that everything is delegated to the Func<> Code review feedback rounds 1 & 2
26 files changed, 718 insertions, 24 deletions
diff --git a/src/System.Web.Http.OData/OData/Builder/ActionConfiguration.cs b/src/System.Web.Http.OData/OData/Builder/ActionConfiguration.cs index d87d1cdc..b7656943 100644 --- a/src/System.Web.Http.OData/OData/Builder/ActionConfiguration.cs +++ b/src/System.Web.Http.OData/OData/Builder/ActionConfiguration.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Web.Http.OData.Properties; +using Microsoft.Data.Edm; namespace System.Web.Http.OData.Builder { @@ -16,6 +18,7 @@ namespace System.Web.Http.OData.Builder { private List<ParameterConfiguration> _parameters = new List<ParameterConfiguration>(); private BindingParameterConfiguration _bindingParameter = null; + private Func<EntityInstanceContext, Uri> _actionLinkFactory = null; /// <summary> /// Create a new ActionConfiguration @@ -40,7 +43,7 @@ namespace System.Web.Http.OData.Builder public override IEnumerable<ParameterConfiguration> Parameters { - get + get { if (_bindingParameter != null) { @@ -67,6 +70,25 @@ namespace System.Web.Http.OData.Builder } /// <summary> + /// Whether this action can always be bound. + /// <example> + /// For example imagine an Watch action that can be bound to a Movie, it might not always be possible to Watch a movie, + /// in which case IsAlwaysBindable would return false. + /// </example> + /// </summary> + public override bool IsAlwaysBindable + { + get + { + if (IsBindable) + { + return _bindingParameter.AlwaysBindable; + } + return false; + } + } + + /// <summary> /// Sets the return type to a single EntityType instance. /// </summary> /// <typeparam name="TEntityType">The type that is an EntityType</typeparam> @@ -77,7 +99,7 @@ namespace System.Web.Http.OData.Builder ModelBuilder.EntitySet<TEntityType>(entitySetName); EntitySet = ModelBuilder.EntitySets.Single(s => s.Name == entitySetName); ReturnType = ModelBuilder.GetTypeConfigurationOrNull(typeof(TEntityType)); - + return this; } @@ -139,11 +161,11 @@ namespace System.Web.Http.OData.Builder } /// <summary> - /// Specifies the bindingParameter name and type, use only if the Action "isBindable". + /// Specifies the bindingParameter name, type and whether it is alwaysBindable, use only if the Action "isBindable". /// </summary> - public ActionConfiguration SetBindingParameter(string name, IEdmTypeConfiguration bindingParameterType) + public ActionConfiguration SetBindingParameter(string name, IEdmTypeConfiguration bindingParameterType, bool alwaysBindable) { - _bindingParameter = new BindingParameterConfiguration(name, bindingParameterType); + _bindingParameter = new BindingParameterConfiguration(name, bindingParameterType, alwaysBindable); return this; } @@ -189,5 +211,27 @@ namespace System.Web.Http.OData.Builder ICollectionTypeConfiguration parameterType = new CollectionTypeConfiguration(elementTypeConfiguration, typeof(IEnumerable<>).MakeGenericType(elementType)); return AddParameter(name, parameterType); } + + /// <summary> + /// Register a factory that creates actions links. + /// </summary> + public ActionConfiguration HasActionLink(Func<EntityInstanceContext, Uri> actionLinkFactory) + { + if (!IsBindable || BindingParameter.TypeConfiguration.Kind != EdmTypeKind.Entity) + { + throw Error.InvalidOperation(SRResources.HasActionLinkRequiresBindToEntity, Name); + } + _actionLinkFactory = actionLinkFactory; + return this; + } + + /// <summary> + /// Retrieves the currently registered action link factory. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Consistent with EF Has/Get pattern")] + public Func<EntityInstanceContext, Uri> GetActionLink() + { + return _actionLinkFactory; + } } } diff --git a/src/System.Web.Http.OData/OData/Builder/ActionLinkBuilder.cs b/src/System.Web.Http.OData/OData/Builder/ActionLinkBuilder.cs new file mode 100644 index 00000000..cf480109 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Builder/ActionLinkBuilder.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace System.Web.Http.OData.Builder +{ + /// <summary> + /// ActionLinkBuilder can be used to annotate an Action. + /// This is how formatters create links to invoke bound actions. + /// </summary> + public class ActionLinkBuilder + { + private Func<EntityInstanceContext, Uri> _actionLinkFactory; + + /// <summary> + /// Create a new ActionLinkBuilder based on an actionLinkFactory. + /// <remarks> + /// If the action is not available the actionLinkFactory delegate should return NULL. + /// </remarks> + /// </summary> + /// <param name="actionLinkFactory">The actionLinkFactory this ActionLinkBuilder should use when building links.</param> + public ActionLinkBuilder(Func<EntityInstanceContext, Uri> actionLinkFactory) + { + if (actionLinkFactory == null) + { + throw Error.ArgumentNull("actionLinkFactory"); + } + _actionLinkFactory = actionLinkFactory; + } + + public virtual Uri BuildActionLink(EntityInstanceContext context) + { + return _actionLinkFactory(context); + } + + /// <summary> + /// Creates an action link factory that builds an action link, but only when appropriate based on the expensiveAvailabilityCheck, and whether expensive checks should be made, + /// which is deduced by looking at the EntityInstanceContext.SkipExpensiveActionAvailabilityChecks property. + /// </summary> + /// <param name="baseFactory">The action link factory that actually builds links if all checks pass.</param> + /// <param name="expensiveAvailabilityCheck">The availability check function that is expensive but when called returns whether the action is available.</param> + /// <returns>The new action link factory.</returns> + public static Func<EntityInstanceContext, Uri> CreateActionLinkFactory(Func<EntityInstanceContext, Uri> baseFactory, Func<EntityInstanceContext, bool> expensiveAvailabilityCheck) + { + return (EntityInstanceContext ctx) => + { + if (ctx.SkipExpensiveAvailabilityChecks) + { + // OData says that if it is too expensive to check availability you should advertize actions + return baseFactory(ctx); + } + else if (expensiveAvailabilityCheck(ctx)) + { + return baseFactory(ctx); + } + else + { + return null; + } + }; + } + } +} diff --git a/src/System.Web.Http.OData/OData/Builder/BindableProcedureFinder.cs b/src/System.Web.Http.OData/OData/Builder/BindableProcedureFinder.cs new file mode 100644 index 00000000..d3307ae0 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Builder/BindableProcedureFinder.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Data.Edm; + +namespace System.Web.Http.OData.Builder +{ + /// <summary> + /// This class builds a cache that allows for efficient look up of bindable procedure by EntityType. + /// </summary> + public class BindableProcedureFinder + { + private Dictionary<IEdmEntityType, List<IEdmFunctionImport>> _map = new Dictionary<IEdmEntityType, List<IEdmFunctionImport>>(); + + /// <summary> + /// Constructs a concurrent cache for looking up bindable procedures for any EntityType in the provided model. + /// </summary> + public BindableProcedureFinder(IEdmModel model) + { + var query = + from ec in model.EntityContainers() + from fi in ec.FunctionImports() + where fi.IsBindable && fi.Parameters.First().Type.TypeKind() == EdmTypeKind.Entity + group fi by fi.Parameters.First().Type.Definition into fgroup + select new { EntityType = fgroup.Key as IEdmEntityType, BindableFunctions = fgroup.ToList() }; + + foreach (var match in query) + { + _map[match.EntityType] = match.BindableFunctions; + } + } + + public virtual IEnumerable<IEdmFunctionImport> FindProcedures(IEdmEntityType entityType) + { + return GetTypeHierarchy(entityType).SelectMany(e => FindDeclaredProcedures(e)); + } + + private IEnumerable<IEdmEntityType> GetTypeHierarchy(IEdmEntityType entityType) + { + IEdmEntityType current = entityType; + while (current != null) + { + yield return current; + current = current.BaseEntityType(); + } + } + + private IEnumerable<IEdmFunctionImport> FindDeclaredProcedures(IEdmEntityType entityType) + { + List<IEdmFunctionImport> results = null; + + if (_map.TryGetValue(entityType, out results)) + { + return results; + } + else + { + return Enumerable.Empty<IEdmFunctionImport>(); + } + } + } +} diff --git a/src/System.Web.Http.OData/OData/Builder/BindingParameterConfiguration.cs b/src/System.Web.Http.OData/OData/Builder/BindingParameterConfiguration.cs index 27a734d7..d43b3214 100644 --- a/src/System.Web.Http.OData/OData/Builder/BindingParameterConfiguration.cs +++ b/src/System.Web.Http.OData/OData/Builder/BindingParameterConfiguration.cs @@ -24,7 +24,15 @@ namespace System.Web.Http.OData.Builder { public const string DefaultBindingParameterName = "bindingParameter"; - public BindingParameterConfiguration(string name, IEdmTypeConfiguration parameterType) + private bool _alwaysBindable; + + /// <summary> + /// Create a BindingParameterConfiguration + /// </summary> + /// <param name="name">The name of the Binding Parameter</param> + /// <param name="parameterType">The type of the Binding Parameter</param> + /// <param name="alwaysBindable">Whether the action can always be bound to instances of the binding parameter.</param> + public BindingParameterConfiguration(string name, IEdmTypeConfiguration parameterType, bool alwaysBindable) : base(name, parameterType) { EdmTypeKind kind = parameterType.Kind; @@ -36,6 +44,16 @@ namespace System.Web.Http.OData.Builder { throw Error.Argument("parameterType", SRResources.InvalidBindingParameterType, parameterType.FullName); } + _alwaysBindable = alwaysBindable; + } + + /// <summary> + /// Indicates whether the BindingParameter is always bindable or not. + /// For example some actions are always available some are only available at certain times or in certain states. + /// </summary> + public bool AlwaysBindable + { + get { return _alwaysBindable; } } } } diff --git a/src/System.Web.Http.OData/OData/Builder/Conventions/ActionLinkGenerationConvention.cs b/src/System.Web.Http.OData/OData/Builder/Conventions/ActionLinkGenerationConvention.cs new file mode 100644 index 00000000..07d9c15d --- /dev/null +++ b/src/System.Web.Http.OData/OData/Builder/Conventions/ActionLinkGenerationConvention.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.Data.Edm; + +namespace System.Web.Http.OData.Builder.Conventions +{ + /// <summary> + /// The ActionLinkGenerationConvention calls action.HasActionLink(..) for all actions that bind to a single entity if they have not previously been configured. + /// The convention uses the <see cref="ODataRouteNames"/>.InvokeBoundAction route to build a link that invokes the action. + /// </summary> + public class ActionLinkGenerationConvention : IProcedureConvention + { + /// <summary> + /// Gets or sets the route name used for addressing entities by their keys. + /// </summary> + public string InvokeBoundActionRouteName { get; set; } + + public void Apply(ProcedureConfiguration configuration, ODataModelBuilder model) + { + ActionConfiguration action = configuration as ActionConfiguration; + + // You can only need to create links for bindable actions that bind to a single entity. + if (action != null && action.IsBindable && action.BindingParameter.TypeConfiguration.Kind == EdmTypeKind.Entity && action.GetActionLink() == null) + { + IEntityTypeConfiguration entityType = action.BindingParameter.TypeConfiguration as IEntityTypeConfiguration; + action.HasActionLink(entityContext => + { + string routeName = InvokeBoundActionRouteName ?? ODataRouteNames.InvokeBoundAction; + string actionLink = entityContext.UrlHelper.Link( + routeName, + new + { + controller = entityContext.EntitySet.Name, + boundId = ConventionsHelpers.GetEntityKeyValue(entityContext, entityType), + odataAction = action.Name + }); + + if (actionLink == null) + { + return null; + } + else + { + return new Uri(actionLink); + } + }); + } + } + } +} diff --git a/src/System.Web.Http.OData/OData/Builder/Conventions/IProcedureConvention.cs b/src/System.Web.Http.OData/OData/Builder/Conventions/IProcedureConvention.cs new file mode 100644 index 00000000..aae1be42 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Builder/Conventions/IProcedureConvention.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace System.Web.Http.OData.Builder.Conventions +{ + /// <summary> + /// Convention to apply to <see cref="ProcedureConfiguration"/> instances in the model + /// </summary> + public interface IProcedureConvention : IConvention + { + void Apply(ProcedureConfiguration configuration, ODataModelBuilder model); + } +} diff --git a/src/System.Web.Http.OData/OData/Builder/EdmModelHelperMethods.cs b/src/System.Web.Http.OData/OData/Builder/EdmModelHelperMethods.cs index d40d91e7..6d4b563c 100644 --- a/src/System.Web.Http.OData/OData/Builder/EdmModelHelperMethods.cs +++ b/src/System.Web.Http.OData/OData/Builder/EdmModelHelperMethods.cs @@ -9,6 +9,7 @@ using Microsoft.Data.Edm; using Microsoft.Data.Edm.Expressions; using Microsoft.Data.Edm.Library; using Microsoft.Data.Edm.Library.Expressions; +using Microsoft.Data.OData; namespace System.Web.Http.OData.Builder { @@ -27,12 +28,16 @@ namespace System.Web.Http.OData.Builder // add types and sets, building an index on the way. Dictionary<string, IEdmStructuredType> edmTypeMap = model.AddTypes(builder.StructuralTypes); Dictionary<string, EdmEntitySet> edmEntitySetMap = model.AddEntitySets(builder.EntitySets, container, edmTypeMap); - + // add procedures - container.AddProcedures(builder.Procedures, edmTypeMap, edmEntitySetMap); + model.AddProcedures(builder.Procedures, container, edmTypeMap, edmEntitySetMap); // finish up model.AddElement(container); + + // build the map from IEdmEntityType to IEdmFunctionImport + model.SetAnnotationValue<BindableProcedureFinder>(model, new BindableProcedureFinder(model)); + return model; } @@ -89,7 +94,7 @@ namespace System.Web.Http.OData.Builder return edmEntitySetMap; } - private static void AddProcedures(this EdmEntityContainer container, IEnumerable<ProcedureConfiguration> configurations, Dictionary<string, IEdmStructuredType> edmTypeMap, Dictionary<string, EdmEntitySet> edmEntitySetMap) + private static void AddProcedures(this IEdmModel model, IEnumerable<ProcedureConfiguration> configurations, EdmEntityContainer container, Dictionary<string, IEdmStructuredType> edmTypeMap, Dictionary<string, EdmEntitySet> edmEntitySetMap) { foreach (ProcedureConfiguration procedure in configurations) { @@ -101,6 +106,19 @@ namespace System.Web.Http.OData.Builder IEdmExpression expression = GetEdmEntitySetExpression(edmEntitySetMap, action); EdmFunctionImport functionImport = new EdmFunctionImport(container, action.Name, returnReference, expression, action.IsSideEffecting, action.IsComposable, action.IsBindable); + if (action.IsBindable) + { + model.SetIsAlwaysBindable(functionImport, action.IsAlwaysBindable); + if (action.BindingParameter.TypeConfiguration.Kind == EdmTypeKind.Entity) + { + Func<EntityInstanceContext, Uri> actionFactory = action.GetActionLink(); + if (actionFactory != null) + { + model.SetAnnotationValue<ActionLinkBuilder>(functionImport, new ActionLinkBuilder(actionFactory)); + } + } + } + foreach (ParameterConfiguration parameter in action.Parameters) { // TODO: http://aspnetwebstack.codeplex.com/workitem/417 @@ -277,5 +295,44 @@ namespace System.Web.Http.OData.Builder return annotation.Url; } } + + internal static IEnumerable<IEdmFunctionImport> GetAvailableProcedures(this IEdmModel model, IEdmEntityType entityType) + { + if (model == null) + { + throw Error.ArgumentNull("model"); + } + + if (entityType == null) + { + throw Error.ArgumentNull("entityType"); + } + + BindableProcedureFinder annotation = model.GetAnnotationValue<BindableProcedureFinder>(model); + if (annotation == null) + { + return Enumerable.Empty<IEdmFunctionImport>(); + } + else + { + return annotation.FindProcedures(entityType); + } + } + + internal static ActionLinkBuilder GetActionLinkBuilder(this IEdmModel model, IEdmFunctionImport action) + { + if (model == null) + { + throw Error.ArgumentNull("model"); + } + + if (action == null) + { + throw Error.ArgumentNull("action"); + } + + ActionLinkBuilder annotation = model.GetAnnotationValue<ActionLinkBuilder>(action); + return annotation; + } } } diff --git a/src/System.Web.Http.OData/OData/Builder/EntityCollectionConfigurationOfTEntityType.cs b/src/System.Web.Http.OData/OData/Builder/EntityCollectionConfigurationOfTEntityType.cs index eb176209..af9c74e8 100644 --- a/src/System.Web.Http.OData/OData/Builder/EntityCollectionConfigurationOfTEntityType.cs +++ b/src/System.Web.Http.OData/OData/Builder/EntityCollectionConfigurationOfTEntityType.cs @@ -11,7 +11,8 @@ namespace System.Web.Http.OData.Builder /// <typeparam name="TEntityType">The EntityType that is the ElementType of the EntityCollection</typeparam> public class EntityCollectionConfiguration<TEntityType> : CollectionTypeConfiguration { - internal EntityCollectionConfiguration(IEntityTypeConfiguration elementType) : base(elementType, typeof(IEnumerable<TEntityType>)) + internal EntityCollectionConfiguration(IEntityTypeConfiguration elementType) + : base(elementType, typeof(IEnumerable<TEntityType>)) { } @@ -23,7 +24,19 @@ namespace System.Web.Http.OData.Builder public ActionConfiguration Action(string name) { ActionConfiguration configuration = new ActionConfiguration(ElementType.ModelBuilder, name); - configuration.SetBindingParameter(BindingParameterConfiguration.DefaultBindingParameterName, this); + configuration.SetBindingParameter(BindingParameterConfiguration.DefaultBindingParameterName, this, alwaysBindable: true); + return configuration; + } + + /// <summary> + /// Creates a new Action that sometimes binds to Collection(EntityType). + /// </summary> + /// <param name="name">The name of the Action</param> + /// <returns>An ActionConfiguration to allow further configuration of the Action.</returns> + public ActionConfiguration TransientAction(string name) + { + ActionConfiguration configuration = new ActionConfiguration(ElementType.ModelBuilder, name); + configuration.SetBindingParameter(BindingParameterConfiguration.DefaultBindingParameterName, this, alwaysBindable: false); return configuration; } } diff --git a/src/System.Web.Http.OData/OData/Builder/EntityTypeConfigurationOfTEntityType.cs b/src/System.Web.Http.OData/OData/Builder/EntityTypeConfigurationOfTEntityType.cs index 967abf94..4512a0b4 100644 --- a/src/System.Web.Http.OData/OData/Builder/EntityTypeConfigurationOfTEntityType.cs +++ b/src/System.Web.Http.OData/OData/Builder/EntityTypeConfigurationOfTEntityType.cs @@ -166,7 +166,19 @@ namespace System.Web.Http.OData.Builder public ActionConfiguration Action(string name) { ActionConfiguration action = new ActionConfiguration(_configuration.ModelBuilder, name); - action.SetBindingParameter(BindingParameterConfiguration.DefaultBindingParameterName, _configuration); + action.SetBindingParameter(BindingParameterConfiguration.DefaultBindingParameterName, _configuration, alwaysBindable: true); + return action; + } + + /// <summary> + /// Create an Action that sometimes binds to this EntityType + /// </summary> + /// <param name="name">The name of the action.</param> + /// <returns>The ActionConfiguration to allow further configuration of the new 'transient' Action.</returns> + public ActionConfiguration TransientAction(string name) + { + ActionConfiguration action = new ActionConfiguration(_configuration.ModelBuilder, name); + action.SetBindingParameter(BindingParameterConfiguration.DefaultBindingParameterName, _configuration, alwaysBindable: false); return action; } diff --git a/src/System.Web.Http.OData/OData/Builder/ODataConventionModelBuilder.cs b/src/System.Web.Http.OData/OData/Builder/ODataConventionModelBuilder.cs index 44e698b6..89b31a3c 100644 --- a/src/System.Web.Http.OData/OData/Builder/ODataConventionModelBuilder.cs +++ b/src/System.Web.Http.OData/OData/Builder/ODataConventionModelBuilder.cs @@ -34,6 +34,15 @@ namespace System.Web.Http.OData.Builder // IEntitySetConvention's new SelfLinksGenerationConvention(), new NavigationLinksGenerationConvention(), + + // IEdmPropertyConvention's + new NotMappedAttributeConvention(), + new RequiredAttributeEdmPropertyConvention(), + new KeyAttributeEdmPropertyConvention(), + new IgnoreDataMemberAttributeEdmPropertyConvention(), + + // IEdmFunctionImportConventions's + new ActionLinkGenerationConvention(), }; // These hashset's keep track of edmtypes/entitysets for which conventions @@ -181,6 +190,11 @@ namespace System.Web.Http.OData.Builder ApplyEntitySetConventions(entitySet); } + foreach (ProcedureConfiguration procedure in Procedures) + { + ApplyProcedureConventions(procedure); + } + return base.GetEdmModel(); } @@ -609,6 +623,14 @@ namespace System.Web.Http.OData.Builder } } + private void ApplyProcedureConventions(ProcedureConfiguration procedure) + { + foreach (IProcedureConvention convention in _conventions.OfType<IProcedureConvention>()) + { + convention.Apply(procedure, this); + } + } + private IStructuralTypeConfiguration GetStructuralTypeOrNull(Type clrType) { return StructuralTypes.Where(edmType => edmType.ClrType == clrType).SingleOrDefault(); diff --git a/src/System.Web.Http.OData/OData/Builder/ProcedureConfiguration.cs b/src/System.Web.Http.OData/OData/Builder/ProcedureConfiguration.cs index b2c62259..76d38f8b 100644 --- a/src/System.Web.Http.OData/OData/Builder/ProcedureConfiguration.cs +++ b/src/System.Web.Http.OData/OData/Builder/ProcedureConfiguration.cs @@ -82,6 +82,14 @@ namespace System.Web.Http.OData.Builder } /// <summary> + /// If the procedure IsBindable is it Always bindable. + /// </summary> + public virtual bool IsAlwaysBindable + { + get { return IsBindable; } + } + + /// <summary> /// Does the procedure have side-effects. /// </summary> public virtual bool IsSideEffecting diff --git a/src/System.Web.Http.OData/OData/EntityInstanceContext.cs b/src/System.Web.Http.OData/OData/EntityInstanceContext.cs index 77d39b4a..2b0ae8d9 100644 --- a/src/System.Web.Http.OData/OData/EntityInstanceContext.cs +++ b/src/System.Web.Http.OData/OData/EntityInstanceContext.cs @@ -19,6 +19,11 @@ namespace System.Web.Http.OData } public EntityInstanceContext(IEdmModel model, IEdmEntitySet entitySet, IEdmEntityType entityType, UrlHelper urlHelper, object entityInstance) + : this(model, entitySet, entityType, urlHelper, entityInstance, skipExpensiveAvailabilityChecks: false) + { + } + + public EntityInstanceContext(IEdmModel model, IEdmEntitySet entitySet, IEdmEntityType entityType, UrlHelper urlHelper, object entityInstance, bool skipExpensiveAvailabilityChecks) { if (model == null) { @@ -40,6 +45,7 @@ namespace System.Web.Http.OData EntityType = entityType; EntityInstance = entityInstance; UrlHelper = urlHelper; + SkipExpensiveAvailabilityChecks = skipExpensiveAvailabilityChecks; } /// <summary> @@ -77,5 +83,14 @@ namespace System.Web.Http.OData /// The setter is not intended to be used other than for unit testing purpose. /// </summary> public UrlHelper UrlHelper { get; set; } + + /// <summary> + /// Gets whether ActionAvailabilityChecks should be performed or not. + /// This is used to tell the formatter whether to check availability of an action before including a link to it. + /// When in a feed we skip this check. + /// + /// The setter is not intended to be used other than for unit testing purposes. + /// </summary> + public bool SkipExpensiveAvailabilityChecks { get; set; } } } diff --git a/src/System.Web.Http.OData/OData/EntityInstanceContextOfTEntityType.cs b/src/System.Web.Http.OData/OData/EntityInstanceContextOfTEntityType.cs index 0e25df5d..9ddeba8a 100644 --- a/src/System.Web.Http.OData/OData/EntityInstanceContextOfTEntityType.cs +++ b/src/System.Web.Http.OData/OData/EntityInstanceContextOfTEntityType.cs @@ -20,7 +20,12 @@ namespace System.Web.Http.OData } public EntityInstanceContext(IEdmModel model, IEdmEntitySet entitySet, IEdmEntityType entityType, UrlHelper urlHelper, TEntityType entityInstance) - : base(model, entitySet, entityType, urlHelper, entityInstance) + : this(model, entitySet, entityType, urlHelper, entityInstance, skipExpensiveActionAvailabilityChecks: false) + { + } + + public EntityInstanceContext(IEdmModel model, IEdmEntitySet entitySet, IEdmEntityType entityType, UrlHelper urlHelper, TEntityType entityInstance, bool skipExpensiveActionAvailabilityChecks) + : base(model, entitySet, entityType, urlHelper, entityInstance, skipExpensiveAvailabilityChecks: skipExpensiveActionAvailabilityChecks) { } diff --git a/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs b/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs index 9e110e21..dcb98be7 100644 --- a/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs +++ b/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs @@ -306,13 +306,18 @@ namespace System.Web.Http.OData.Formatter IODataResponseMessage responseMessage = new ODataMessageWrapper(writeStream); + // TODO: Issue 483: http://aspnetwebstack.codeplex.com/workitem/483 + // We need to set the MetadataDocumentUri when this property is added to ODataMessageWriterSettings as + // part of the JSON Light work. + // This is required so ODataLib can coerce AbsoluteUri's into RelativeUri's when appropriate in JSON Light. ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings() { BaseUri = baseAddress, Version = version, Indent = true, - DisableMessageStreamDisposal = true, + DisableMessageStreamDisposal = true }; + if (contentHeaders != null && contentHeaders.ContentType != null) { writerSettings.SetContentType(contentHeaders.ContentType.ToString(), Encoding.UTF8.WebName); @@ -327,6 +332,7 @@ namespace System.Web.Http.OData.Formatter RootProjectionNode = rootProjectionNode, CurrentProjectionNode = rootProjectionNode, ServiceOperationName = operationName, + SkipExpensiveAvailabilityChecks = serializer.ODataPayloadKind == ODataPayloadKind.Feed, Request = Request }; diff --git a/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataEntityTypeSerializer.cs b/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataEntityTypeSerializer.cs index c5d431c9..a58a7045 100644 --- a/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataEntityTypeSerializer.cs +++ b/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataEntityTypeSerializer.cs @@ -67,12 +67,13 @@ namespace System.Web.Http.OData.Formatter.Serialization private void WriteEntry(object graph, IEnumerable<ODataProperty> propertyBag, ODataWriter writer, ODataSerializerContext writeContext) { IEdmEntityType entityType = _edmEntityTypeReference.EntityDefinition(); - EntityInstanceContext entityInstanceContext = new EntityInstanceContext(SerializerProvider.EdmModel, writeContext.EntitySet, entityType, writeContext.UrlHelper, graph); + EntityInstanceContext entityInstanceContext = new EntityInstanceContext(SerializerProvider.EdmModel, writeContext.EntitySet, entityType, writeContext.UrlHelper, graph, writeContext.SkipExpensiveAvailabilityChecks); ODataEntry entry = new ODataEntry { TypeName = _edmEntityTypeReference.FullName(), Properties = propertyBag, + Actions = CreateActions(entityInstanceContext) }; if (writeContext.EntitySet != null) @@ -187,5 +188,34 @@ namespace System.Web.Http.OData.Formatter.Serialization return properties; } + + private static IEnumerable<ODataAction> CreateActions(EntityInstanceContext context) + { + return context.EdmModel.GetAvailableProcedures(context.EntityType) + .Select(action => CreateODataAction(action, context)) + .Where(action => action != null); + } + + private static ODataAction CreateODataAction(IEdmFunctionImport action, EntityInstanceContext context) + { + ActionLinkBuilder builder = context.EdmModel.GetActionLinkBuilder(action); + if (builder != null) + { + Uri target = builder.BuildActionLink(context); + if (target != null) + { + Uri baseUri = new Uri(context.UrlHelper.Link(ODataRouteNames.Metadata, null)); + Uri metadata = new Uri(baseUri, "#" + action.Container.Name + "." + action.Name); + + return new ODataAction + { + Metadata = metadata, + Target = target, + Title = action.Name + }; + } + } + return null; + } } } diff --git a/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs b/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs index 5323c23f..9b00c59f 100644 --- a/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs +++ b/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs @@ -42,5 +42,10 @@ namespace System.Web.Http.OData.Formatter.Serialization /// The HttpRequestMessage can then be used by ODataSerializers to learn more about the Request that triggered the serialization /// </summary> public HttpRequestMessage Request { get; set; } + + /// <summary> + /// Get or sets whether expensive links should be calculated. + /// </summary> + public bool SkipExpensiveAvailabilityChecks { get; set; } } } diff --git a/src/System.Web.Http.OData/ODataRouteNames.cs b/src/System.Web.Http.OData/ODataRouteNames.cs index 43bb4bb8..cfbd1874 100644 --- a/src/System.Web.Http.OData/ODataRouteNames.cs +++ b/src/System.Web.Http.OData/ODataRouteNames.cs @@ -41,5 +41,10 @@ namespace System.Web.Http /// Route name for the route used for addressing root level entity sets (with parentheses to support WCF dataservices client). /// </summary> public static readonly string DefaultWithParentheses = "OData.DefaultWithParentheses"; + + /// <summary> + /// Route name for the route used for addressing an action bound to an entity's editlink + /// </summary> + public static readonly string InvokeBoundAction = "OData.InvokeBoundAction"; } } diff --git a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs index e661c268..9239f8fe 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs +++ b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs @@ -457,6 +457,15 @@ namespace System.Web.Http.OData.Properties { } /// <summary> + /// Looks up a localized string similar to To register an action link factory, actions must be bindable to a single entity. Action '{0}' does not meet this requirement.. + /// </summary> + internal static string HasActionLinkRequiresBindToEntity { + get { + return ResourceManager.GetString("HasActionLinkRequiresBindToEntity", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Invalid bindingParameter type '{0}'. A bindingParameter must be either an EntityType or a Collection of EntityTypes.. /// </summary> internal static string InvalidBindingParameterType { diff --git a/src/System.Web.Http.OData/Properties/SRResources.resx b/src/System.Web.Http.OData/Properties/SRResources.resx index a501622f..280bff86 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.resx +++ b/src/System.Web.Http.OData/Properties/SRResources.resx @@ -429,4 +429,7 @@ <data name="ModelBinderUtil_ModelMetadataCannotBeNull" xml:space="preserve"> <value>The binding context cannot have a null ModelMetadata.</value> </data> + <data name="HasActionLinkRequiresBindToEntity" xml:space="preserve"> + <value>To register an action link factory, actions must be bindable to a single entity. Action '{0}' does not meet this requirement.</value> + </data> </root>
\ No newline at end of file diff --git a/src/System.Web.Http.OData/System.Web.Http.OData.csproj b/src/System.Web.Http.OData/System.Web.Http.OData.csproj index 54bf0feb..18063b81 100644 --- a/src/System.Web.Http.OData/System.Web.Http.OData.csproj +++ b/src/System.Web.Http.OData/System.Web.Http.OData.csproj @@ -104,6 +104,10 @@ <Link>Common\Error.cs</Link> </Compile> <Compile Include="GlobalSuppressions.cs" /> + <Compile Include="OData\Builder\ActionLinkBuilder.cs" /> + <Compile Include="OData\Builder\BindableProcedureFinder.cs" /> + <Compile Include="OData\Builder\Conventions\ActionLinkGenerationConvention.cs" /> + <Compile Include="OData\Builder\Conventions\IProcedureConvention.cs" /> <Compile Include="OData\ODataActionParameters.cs" /> <Compile Include="OData\Builder\ActionConfiguration.cs" /> <Compile Include="OData\Builder\BindingParameterConfiguration.cs" /> diff --git a/test/System.Web.Http.OData.Test/OData/Builder/ActionConfigurationTest.cs b/test/System.Web.Http.OData.Test/OData/Builder/ActionConfigurationTest.cs index 25f5b3e9..eb9899d4 100644 --- a/test/System.Web.Http.OData.Test/OData/Builder/ActionConfigurationTest.cs +++ b/test/System.Web.Http.OData.Test/OData/Builder/ActionConfigurationTest.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. using System.Linq; +using System.Net.Http; +using System.Web.Http.Hosting; using System.Web.Http.OData.Builder.TestModels; +using System.Web.Http.Routing; using Microsoft.Data.Edm; using Microsoft.Data.Edm.Expressions; +using Microsoft.Data.OData; using Microsoft.TestCommon; namespace System.Web.Http.OData.Builder @@ -43,7 +47,7 @@ namespace System.Web.Http.OData.Builder ODataModelBuilder builder = new ODataModelBuilder(); ODataModelBuilder builder2 = new ODataModelBuilder(); ProcedureConfiguration toRemove = new ActionConfiguration(builder2, "ToRemove"); - + // Act bool removedByName = builder.RemoveProcedure("ToRemove"); bool removed = builder.RemoveProcedure(toRemove); @@ -87,10 +91,10 @@ namespace System.Web.Http.OData.Builder // Arrange // Act ODataModelBuilder builder = new ODataModelBuilder(); - + ActionConfiguration createAddress = new ActionConfiguration(builder, "CreateAddress"); createAddress.Returns<Address>(); - + ActionConfiguration createAddresses = new ActionConfiguration(builder, "CreateAddresses"); createAddresses.ReturnsCollection<Address>(); @@ -150,6 +154,7 @@ namespace System.Web.Http.OData.Builder // Assert Assert.True(sendEmail.IsBindable); + Assert.True(sendEmail.IsAlwaysBindable); Assert.NotNull(sendEmail.Parameters); Assert.Equal(1, sendEmail.Parameters.Count()); Assert.Equal(BindingParameterConfiguration.DefaultBindingParameterName, sendEmail.Parameters.Single().Name); @@ -167,6 +172,7 @@ namespace System.Web.Http.OData.Builder // Assert Assert.True(sendEmail.IsBindable); + Assert.True(sendEmail.IsAlwaysBindable); Assert.NotNull(sendEmail.Parameters); Assert.Equal(1, sendEmail.Parameters.Count()); Assert.Equal(BindingParameterConfiguration.DefaultBindingParameterName, sendEmail.Parameters.Single().Name); @@ -174,6 +180,19 @@ namespace System.Web.Http.OData.Builder } [Fact] + public void CanCreateTransientAction() + { + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration<Customer> customer = builder.Entity<Customer>(); + customer.TransientAction("Reward"); + + ProcedureConfiguration action = builder.Procedures.SingleOrDefault(); + Assert.NotNull(action); + Assert.True(action.IsBindable); + Assert.False(action.IsAlwaysBindable); + } + + [Fact] public void CanCreateActionWithNonBindingParameters() { // Arrange @@ -228,6 +247,7 @@ namespace System.Web.Http.OData.Builder Assert.False(action.IsComposable); Assert.True(action.IsSideEffecting); Assert.True(action.IsBindable); + Assert.True(model.IsAlwaysBindable(action)); Assert.Equal("ActionName", action.Name); Assert.Null(action.ReturnType); Assert.Equal(1, action.Parameters.Count()); @@ -256,6 +276,7 @@ namespace System.Web.Http.OData.Builder Assert.False(action.IsComposable); Assert.True(action.IsSideEffecting); Assert.False(action.IsBindable); + Assert.False(model.IsAlwaysBindable(action)); Assert.Equal("ActionName", action.Name); Assert.NotNull(action.ReturnType); Assert.NotNull(action.EntitySet); @@ -263,5 +284,89 @@ namespace System.Web.Http.OData.Builder Assert.Equal(typeof(Customer).FullName, (action.EntitySet as IEdmEntitySetReferenceExpression).ReferencedEntitySet.ElementType.FullName()); Assert.Empty(action.Parameters); } + + [Fact] + public void CanCreateEdmModel_WithTransientBindableAction() + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration<Customer> customer = builder.Entity<Customer>(); + customer.HasKey(c => c.CustomerId); + customer.Property(c => c.Name); + // Act + ActionConfiguration sendEmail = customer.TransientAction("ActionName"); + IEdmModel model = builder.GetEdmModel(); + + // Assert + IEdmEntityContainer container = model.EntityContainers().SingleOrDefault(); + Assert.NotNull(container); + Assert.Equal(1, container.Elements.OfType<IEdmFunctionImport>().Count()); + IEdmFunctionImport action = container.Elements.OfType<IEdmFunctionImport>().Single(); + Assert.True(action.IsBindable); + Assert.False(model.IsAlwaysBindable(action)); + } + + [Fact] + public void CanManuallyConfigureActionLinkFactory() + { + // Arrange + string uriTemplate = "http://server/service/Customers({0})/Reward"; + Uri expectedUri = new Uri(string.Format(uriTemplate, 1)); + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration<Customer> customer = builder.EntitySet<Customer>("Customers").EntityType; + customer.HasKey(c => c.CustomerId); + customer.Property(c => c.Name); + + // Act + ActionConfiguration reward = customer.Action("Reward"); + reward.HasActionLink(ctx => new Uri(string.Format(uriTemplate, (ctx.EntityInstance as Customer).CustomerId))); + IEdmModel model = builder.GetEdmModel(); + IEdmEntityType customerType = model.SchemaElements.OfType<IEdmEntityType>().SingleOrDefault(); + EntityInstanceContext<Customer> context = new EntityInstanceContext<Customer>(model, null, customerType, null, new Customer { CustomerId = 1 }); + IEdmFunctionImport rewardAction = model.SchemaElements.OfType<IEdmEntityContainer>().SingleOrDefault().FunctionImports().SingleOrDefault(); + ActionLinkBuilder actionLinkBuilder = model.GetAnnotationValue<ActionLinkBuilder>(rewardAction); + + //Assert + Assert.Equal(expectedUri, reward.GetActionLink()(context)); + Assert.NotNull(actionLinkBuilder); + Assert.Equal(expectedUri, actionLinkBuilder.BuildActionLink(context)); + } + + [Fact] + public void WhenActionLinksNotManuallyConfigured_ConventionBasedBuilderUsesConventions() + { + // Arrange + string uriTemplate = "http://server/Movies({0})/Watch"; + Uri expectedUri = new Uri(string.Format(uriTemplate, 1)); + ODataModelBuilder builder = new ODataConventionModelBuilder(); + EntityTypeConfiguration<Movie> movie = builder.EntitySet<Movie>("Movies").EntityType; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://server/Movies"); + HttpConfiguration configuration = new HttpConfiguration(); + configuration.Routes.MapHttpRoute(ODataRouteNames.InvokeBoundAction, "{controller}({boundId})/{odataAction}"); + request.Properties[HttpPropertyKeys.HttpConfigurationKey] = configuration; + request.Properties[HttpPropertyKeys.HttpRouteDataKey] = new HttpRouteData(new HttpRoute()); + UrlHelper urlHelper = new UrlHelper(request); + + // Act + ActionConfiguration watch = movie.Action("Watch"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntityType movieType = model.SchemaElements.OfType<IEdmEntityType>().SingleOrDefault(); + IEdmEntityContainer container = model.SchemaElements.OfType<IEdmEntityContainer>().SingleOrDefault(); + IEdmFunctionImport watchAction = container.FunctionImports().SingleOrDefault(); + IEdmEntitySet entitySet = container.EntitySets().SingleOrDefault(); + EntityInstanceContext<Movie> context = new EntityInstanceContext<Movie>(model, entitySet, movieType, urlHelper, new Movie { ID = 1, Name = "Avatar" }, false); + ActionLinkBuilder actionLinkBuilder = model.GetAnnotationValue<ActionLinkBuilder>(watchAction); + + //Assert + Assert.Equal(expectedUri, watch.GetActionLink()(context)); + Assert.NotNull(actionLinkBuilder); + Assert.Equal(expectedUri, actionLinkBuilder.BuildActionLink(context)); + } + + public class Movie + { + public int ID { get; set; } + public string Name { get; set; } + } } } diff --git a/test/System.Web.Http.OData.Test/OData/Builder/BindableProcedureFinderAnnotationTest.cs b/test/System.Web.Http.OData.Test/OData/Builder/BindableProcedureFinderAnnotationTest.cs new file mode 100644 index 00000000..c65e284c --- /dev/null +++ b/test/System.Web.Http.OData.Test/OData/Builder/BindableProcedureFinderAnnotationTest.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.Data.Edm; +using Microsoft.TestCommon; + +namespace System.Web.Http.OData.Builder +{ + public class BindableProcedureFinderAnnotationTest + { + [Fact] + public void CanBuildBoundProcedureCacheForIEdmModel() + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration<Customer> customer = builder.EntitySet<Customer>("Customers").EntityType; + customer.HasKey(c => c.ID); + customer.Property(c => c.Name); + customer.ComplexProperty(c => c.Address); + + EntityTypeConfiguration<Movie> movie = builder.EntitySet<Movie>("Movies").EntityType; + movie.HasKey(m => m.ID); + movie.HasKey(m => m.Name); + EntityTypeConfiguration<Blockbuster> blockBuster = builder.Entity<Blockbuster>().DerivesFrom<Movie>(); + IEntityTypeConfiguration movieConfiguration = builder.StructuralTypes.OfType<IEntityTypeConfiguration>().Single(t => t.Name == "Movie"); + + // build actions that are bindable to a single entity + customer.Action("InCache1_CustomerAction"); + customer.Action("InCache2_CustomerAction"); + movie.Action("InCache3_MovieAction"); + ActionConfiguration incache4_MovieAction = new ActionConfiguration(builder, "InCache4_MovieAction"); + incache4_MovieAction.SetBindingParameter("bindingParameter", movieConfiguration, true); + blockBuster.Action("InCache5_BlockbusterAction"); + + // build actions that are either: bindable to a collection of entities, have no parameter, have only complex parameter + customer.Collection.Action("NotInCache1_CustomersAction"); + movie.Collection.Action("NotInCache2_MoviesAction"); + ActionConfiguration notInCache3_NoParameters = new ActionConfiguration(builder, "NotInCache3_NoParameters"); + ActionConfiguration notInCache4_AddressParameter = new ActionConfiguration(builder, "NotInCache4_AddressParameter"); + notInCache4_AddressParameter.Parameter<Address>("address"); + + IEdmModel model = builder.GetEdmModel(); + IEdmEntityType customerType = model.SchemaElements.OfType<IEdmEntityType>().Single(e => e.Name == "Customer"); + IEdmEntityType movieType = model.SchemaElements.OfType<IEdmEntityType>().Single(e => e.Name == "Movie"); + IEdmEntityType blockBusterType = model.SchemaElements.OfType<IEdmEntityType>().Single(e => e.Name == "Blockbuster"); + + // Act + BindableProcedureFinder annotation = new BindableProcedureFinder(model); + IEdmFunctionImport[] movieActions = annotation.FindProcedures(movieType).ToArray(); + IEdmFunctionImport[] customerActions = annotation.FindProcedures(customerType).ToArray(); + IEdmFunctionImport[] blockBusterActions = annotation.FindProcedures(blockBusterType).ToArray(); + + // Assert + Assert.Equal(2, customerActions.Length); + Assert.NotNull(customerActions.SingleOrDefault(a => a.Name == "InCache1_CustomerAction")); + Assert.NotNull(customerActions.SingleOrDefault(a => a.Name == "InCache2_CustomerAction")); + Assert.Equal(2, movieActions.Length); + Assert.NotNull(movieActions.SingleOrDefault(a => a.Name == "InCache3_MovieAction")); + Assert.NotNull(movieActions.SingleOrDefault(a => a.Name == "InCache4_MovieAction")); + Assert.Equal(3, blockBusterActions.Length); + Assert.NotNull(blockBusterActions.SingleOrDefault(a => a.Name == "InCache3_MovieAction")); + Assert.NotNull(blockBusterActions.SingleOrDefault(a => a.Name == "InCache4_MovieAction")); + Assert.NotNull(blockBusterActions.SingleOrDefault(a => a.Name == "InCache5_BlockbusterAction")); + } + + public class Movie + { + public int ID { get; set; } + public string Name { get; set; } + } + + public class Blockbuster : Movie + { + } + + public class Customer + { + public int ID { get; set; } + public string Name { get; set; } + public Address Address { get; set; } + } + + public class Address + { + public string Street { get; set; } + public string City { get; set; } + public string State { get; set; } + public int ZipCode { get; set; } + } + } +} diff --git a/test/System.Web.Http.OData.Test/OData/Builder/ODataModelBuilderTest.cs b/test/System.Web.Http.OData.Test/OData/Builder/ODataModelBuilderTest.cs index a1c53666..67abe08c 100644 --- a/test/System.Web.Http.OData.Test/OData/Builder/ODataModelBuilderTest.cs +++ b/test/System.Web.Http.OData.Test/OData/Builder/ODataModelBuilderTest.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Web.Http.OData.TestCommon.Models; +using Microsoft.Data.Edm; using Microsoft.TestCommon; namespace System.Web.Http.OData.Builder @@ -76,5 +77,26 @@ namespace System.Web.Http.OData.Builder builder.RemoveProcedure("Format"); }); } + + [Fact] + public void BuilderIncludesMapFromEntityTypeToBindableProcedures() + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration<Customer> customer = builder.EntitySet<Customer>("Customers").EntityType; + customer.HasKey(c => c.Id); + customer.Property(c => c.Name); + customer.Action("Reward"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntityType customerType = model.SchemaElements.OfType<IEdmEntityType>().SingleOrDefault(); + + // Act + BindableProcedureFinder finder = model.GetAnnotationValue<BindableProcedureFinder>(model); + + // Assert + Assert.NotNull(finder); + Assert.NotNull(finder.FindProcedures(customerType).SingleOrDefault()); + Assert.Equal("Reward", finder.FindProcedures(customerType).SingleOrDefault().Name); + } } } diff --git a/test/System.Web.Http.OData.Test/OData/Builder/ParameterConfigurationTest.cs b/test/System.Web.Http.OData.Test/OData/Builder/ParameterConfigurationTest.cs index c74cbf8b..e03e02ea 100644 --- a/test/System.Web.Http.OData.Test/OData/Builder/ParameterConfigurationTest.cs +++ b/test/System.Web.Http.OData.Test/OData/Builder/ParameterConfigurationTest.cs @@ -17,7 +17,7 @@ namespace System.Web.Http.OData.Builder // Act & Assert ArgumentException exception = Assert.Throws<ArgumentException>(() => { - BindingParameterConfiguration configuration = new BindingParameterConfiguration("name", builder.GetTypeConfigurationOrNull(typeof(Address))); + BindingParameterConfiguration configuration = new BindingParameterConfiguration("name", builder.GetTypeConfigurationOrNull(typeof(Address)), true); }); Assert.True(exception.Message.Contains(string.Format("'{0}'", typeof(Address).FullName))); Assert.Equal("parameterType", exception.ParamName); diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs b/test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs index 11f4aa41..819945b1 100644 --- a/test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs +++ b/test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs @@ -6,6 +6,7 @@ using System.Net.Http.Headers; using System.Web.Http.OData.Builder; using Microsoft.Data.Edm; using Microsoft.TestCommon; +using Newtonsoft.Json.Linq; namespace System.Web.Http.OData.Formatter { @@ -25,9 +26,9 @@ namespace System.Web.Http.OData.Formatter _configuration.Formatters.Clear(); _configuration.SetODataFormatter(_formatter); - _configuration.Routes.MapHttpRoute("default", "{action}", new { Controller = "ODataActions" }); + _configuration.Routes.MapHttpRoute(ODataRouteNames.Metadata, "$metadata"); _configuration.Routes.MapHttpRoute(ODataRouteNames.GetById, "{controller}({id})"); - _configuration.Routes.MapHttpRoute(ODataRouteNames.Default, "{controller}"); + _configuration.Routes.MapHttpRoute(ODataRouteNames.InvokeBoundAction, "{controller}({boundId})/{odataAction}"); _server = new HttpServer(_configuration); _client = new HttpClient(_server); @@ -36,7 +37,7 @@ namespace System.Web.Http.OData.Formatter [Fact] public void Can_dispatch_actionPayload_to_action() { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/DoSomething"); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Customers(1)/DoSomething"); request.Headers.Add("accept", "application/json;odata=verbose"); string payload = @"{ ""p1"": 1, @@ -52,6 +53,31 @@ namespace System.Web.Http.OData.Formatter response.EnsureSuccessStatusCode(); } + [Fact] + public void Response_includes_action_link() + { + string editLink = "http://localhost/Customers(1)"; + string expectedTarget = editLink + "/DoSomething"; + string expectedMetadata = "http://localhost/$metadata#Container.DoSomething"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, editLink); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata=verbose")); + HttpResponseMessage response = _client.SendAsync(request).Result; + string responseString = response.Content.ReadAsStringAsync().Result; + + dynamic result = JObject.Parse(responseString); + result = result.d.__metadata.actions; + + JObject allActions = result as JObject; + JArray doSomethings = allActions[expectedMetadata] as JArray; + Assert.NotNull(doSomethings); + Assert.Equal(1, doSomethings.Count); + dynamic doSomething = doSomethings[0]; + Assert.NotNull(doSomething); + Assert.Equal(expectedTarget, (string)doSomething.target); + Assert.Equal("DoSomething", (string)doSomething.title); + } + private IEdmModel GetModel() { ODataModelBuilder builder = new ODataConventionModelBuilder(); @@ -81,11 +107,17 @@ namespace System.Web.Http.OData.Formatter } } - public class ODataActionsController : ApiController + public class CustomersController : ApiController { + public ODataActionTests.Customer GetById(int id) + { + return new ODataActionTests.Customer { ID = id, Name = "Name" + id.ToString() }; + } + [HttpPost] - public bool DoSomething(ODataActionParameters parameters) + public bool DoSomething(int boundId, ODataActionParameters parameters) { + Assert.Equal(1, boundId); Assert.Equal(1, parameters["p1"]); ValidateAddress(parameters["p2"] as ODataActionTests.Address); ValidateNumbers(parameters["p3"] as IList<string>); diff --git a/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj b/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj index 93fc186c..d2a1a89e 100644 --- a/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj +++ b/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj @@ -104,6 +104,7 @@ <DependentUpon>BaselineResource.resx</DependentUpon> </Compile> <Compile Include="OData\Builder\ActionConfigurationTest.cs" /> + <Compile Include="OData\Builder\BindableProcedureFinderAnnotationTest.cs" /> <Compile Include="OData\Builder\ParameterConfigurationTest.cs" /> <Compile Include="OData\Builder\CollectionPropertyConfigurationTest.cs" /> <Compile Include="OData\Builder\TestModels\EnumModel.cs" /> |