Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/aspnetwebstack.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex James <Alex@base4.net>2012-09-28 21:13:30 +0400
committerAlex James <Alex@base4.net>2012-10-08 22:58:10 +0400
commit7472815eec705c47a55a7a9c7e4f18545bf5a063 (patch)
tree3e8eef4ad77548dce03831a1108015118d700dc7
parentc1b5a3ef6b14685713da1e35b806f2c77a684dce (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
-rw-r--r--src/System.Web.Http.OData/OData/Builder/ActionConfiguration.cs54
-rw-r--r--src/System.Web.Http.OData/OData/Builder/ActionLinkBuilder.cs61
-rw-r--r--src/System.Web.Http.OData/OData/Builder/BindableProcedureFinder.cs64
-rw-r--r--src/System.Web.Http.OData/OData/Builder/BindingParameterConfiguration.cs20
-rw-r--r--src/System.Web.Http.OData/OData/Builder/Conventions/ActionLinkGenerationConvention.cs50
-rw-r--r--src/System.Web.Http.OData/OData/Builder/Conventions/IProcedureConvention.cs12
-rw-r--r--src/System.Web.Http.OData/OData/Builder/EdmModelHelperMethods.cs63
-rw-r--r--src/System.Web.Http.OData/OData/Builder/EntityCollectionConfigurationOfTEntityType.cs17
-rw-r--r--src/System.Web.Http.OData/OData/Builder/EntityTypeConfigurationOfTEntityType.cs14
-rw-r--r--src/System.Web.Http.OData/OData/Builder/ODataConventionModelBuilder.cs22
-rw-r--r--src/System.Web.Http.OData/OData/Builder/ProcedureConfiguration.cs8
-rw-r--r--src/System.Web.Http.OData/OData/EntityInstanceContext.cs15
-rw-r--r--src/System.Web.Http.OData/OData/EntityInstanceContextOfTEntityType.cs7
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs8
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/Serialization/ODataEntityTypeSerializer.cs32
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs5
-rw-r--r--src/System.Web.Http.OData/ODataRouteNames.cs5
-rw-r--r--src/System.Web.Http.OData/Properties/SRResources.Designer.cs9
-rw-r--r--src/System.Web.Http.OData/Properties/SRResources.resx3
-rw-r--r--src/System.Web.Http.OData/System.Web.Http.OData.csproj4
-rw-r--r--test/System.Web.Http.OData.Test/OData/Builder/ActionConfigurationTest.cs111
-rw-r--r--test/System.Web.Http.OData.Test/OData/Builder/BindableProcedureFinderAnnotationTest.cs91
-rw-r--r--test/System.Web.Http.OData.Test/OData/Builder/ODataModelBuilderTest.cs22
-rw-r--r--test/System.Web.Http.OData.Test/OData/Builder/ParameterConfigurationTest.cs2
-rw-r--r--test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs42
-rw-r--r--test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj1
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 &apos;{0}&apos; 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 &apos;{0}&apos;. 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" />